diff --git a/CHANGELOG.md b/CHANGELOG.md index 348d412f9..c918cf4e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co - Fixed the app closing when WebView's page process crashed ([@AntsyLich](https://github.com/AntsyLich)) - Fixed installing or updating an extension sometimes cancelling the install of another one ([@AntsyLich](https://github.com/AntsyLich)) - Fixed updating from v0.20.x taking up to hours on libraries with many duplicate entries ([@AntsyLich](https://github.com/AntsyLich)) +- Fixed the app staying on the updating screen for good when updating its data failed, it now shows what went wrong, and updating failing on leftover data of deleted entries ([@AntsyLich](https://github.com/AntsyLich)) ## [v0.21.0] - 2026-10-12 ### Added diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index a144031c0..3984efa4f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -179,6 +179,9 @@ class MainActivity : BaseActivity() { Migrator.awaitAndRelease() isAppMigrating.value = false } + // Uncaught on purpose: a failed database migration otherwise leaves the migrating screen up for good, + // while the crash screen tells what went wrong and lets the logs be shared + lifecycleScope.launch { databaseRepository.awaitOpen() } // Do not let the launcher create a new activity http://stackoverflow.com/questions/16283079 if (!isTaskRoot) { diff --git a/app/src/main/java/mihon/core/migration/MigrationJobFactory.kt b/app/src/main/java/mihon/core/migration/MigrationJobFactory.kt index fb2c3787c..d9d6a45f6 100644 --- a/app/src/main/java/mihon/core/migration/MigrationJobFactory.kt +++ b/app/src/main/java/mihon/core/migration/MigrationJobFactory.kt @@ -1,9 +1,11 @@ package mihon.core.migration +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.async +import logcat.LogPriority import tachiyomi.core.common.util.system.logcat class MigrationJobFactory( @@ -20,7 +22,18 @@ class MigrationJobFactory( } async { val prev = acc.await() - migration(migrationContext) || prev + // A failure would cancel the rest of the chain and leave the app waiting on it for good + val ran = try { + migration(migrationContext) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) { + "Migration failed: { name = ${migration::class.simpleName}, version = ${migration.version} }" + } + false + } + ran || prev } } else { logcat { diff --git a/data/src/main/java/mihon/data/database/DatabaseRepositoryImpl.kt b/data/src/main/java/mihon/data/database/DatabaseRepositoryImpl.kt index 2dbd4e1e7..200b31722 100644 --- a/data/src/main/java/mihon/data/database/DatabaseRepositoryImpl.kt +++ b/data/src/main/java/mihon/data/database/DatabaseRepositoryImpl.kt @@ -1,16 +1,29 @@ package mihon.data.database +import app.cash.sqldelight.db.QueryResult +import app.cash.sqldelight.db.SqlDriver import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.Inject import dev.zacsweers.metro.SingleIn +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.withContext import mihon.domain.database.repository.DatabaseRepository @Inject @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) -class DatabaseRepositoryImpl : DatabaseRepository { +class DatabaseRepositoryImpl( + // Lazy since the driver reports its migration state here + private val driver: Lazy, +) : DatabaseRepository { override val isMigrating = MutableStateFlow(false) + + override suspend fun awaitOpen() { + withContext(Dispatchers.IO) { + driver.value.executeQuery(null, "SELECT 1", { QueryResult.Value(Unit) }, 0).await() + } + } } diff --git a/data/src/main/sqldelight/tachiyomi/migrations/17.sqm b/data/src/main/sqldelight/tachiyomi/migrations/17.sqm index 55bd90652..282d0726a 100644 --- a/data/src/main/sqldelight/tachiyomi/migrations/17.sqm +++ b/data/src/main/sqldelight/tachiyomi/migrations/17.sqm @@ -4,6 +4,19 @@ import kotlin.String; import kotlin.collections.List; import kotlinx.serialization.json.JsonObject; +-- --------------------------------------------------------------------------------------------------------- +-- orphans +-- --------------------------------------------------------------------------------------------------------- + +-- Rows pointing at an entry, chapter or category that no longer exists were written while foreign keys weren't +-- enforced. Nothing can reach them, and copied over they'd fail the foreign key check that ends the migration. +DELETE FROM chapters WHERE manga_id NOT IN (SELECT _id FROM mangas); +DELETE FROM history WHERE chapter_id NOT IN (SELECT _id FROM chapters); +DELETE FROM mangas_categories +WHERE manga_id NOT IN (SELECT _id FROM mangas) OR category_id NOT IN (SELECT _id FROM categories); +DELETE FROM manga_sync WHERE manga_id NOT IN (SELECT _id FROM mangas); +DELETE FROM excluded_scanlators WHERE manga_id NOT IN (SELECT _id FROM mangas); + -- --------------------------------------------------------------------------------------------------------- -- table - manga -- --------------------------------------------------------------------------------------------------------- diff --git a/domain/src/main/java/mihon/domain/database/repository/DatabaseRepository.kt b/domain/src/main/java/mihon/domain/database/repository/DatabaseRepository.kt index dfc52725c..fd64f03e2 100644 --- a/domain/src/main/java/mihon/domain/database/repository/DatabaseRepository.kt +++ b/domain/src/main/java/mihon/domain/database/repository/DatabaseRepository.kt @@ -9,4 +9,9 @@ interface DatabaseRepository { * app update. */ val isMigrating: StateFlow + + /** + * Opens the database, migrating it if needed, and throws what made the migration fail. + */ + suspend fun awaitOpen() }