mirror of
https://github.com/gkd-kit/gkd.git
synced 2026-09-03 07:19:59 +08:00
refactor(db): prepare Room 3 driver migration
This commit is contained in:
@@ -7,6 +7,9 @@ plugins {
|
||||
|
||||
android {
|
||||
namespace = "li.gkd.db"
|
||||
defaultConfig {
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
}
|
||||
|
||||
room {
|
||||
@@ -20,7 +23,12 @@ dependencies {
|
||||
implementation(libs.androidx.room.ktx)
|
||||
api(libs.androidx.room.paging)
|
||||
ksp(libs.androidx.room.compiler)
|
||||
implementation(libs.androidx.sqlite.framework)
|
||||
|
||||
api(libs.kotlinx.serialization.core)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
|
||||
androidTestImplementation(libs.androidx.room.testing)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.test.runner)
|
||||
}
|
||||
|
||||
177
gkd-db/src/androidTest/kotlin/li/gkd/db/AppDbMigrationTest.kt
Normal file
177
gkd-db/src/androidTest/kotlin/li/gkd/db/AppDbMigrationTest.kt
Normal file
@@ -0,0 +1,177 @@
|
||||
package li.gkd.db
|
||||
|
||||
import androidx.room.Room
|
||||
import androidx.room.immediateTransaction
|
||||
import androidx.room.testing.MigrationTestHelper
|
||||
import androidx.room.useWriterConnection
|
||||
import androidx.sqlite.SQLiteConnection
|
||||
import androidx.sqlite.execSQL
|
||||
import androidx.sqlite.driver.AndroidSQLiteDriver
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class AppDbMigrationTest {
|
||||
private val instrumentation = InstrumentationRegistry.getInstrumentation()
|
||||
private val context = instrumentation.targetContext
|
||||
private val databaseNames = mutableSetOf<String>()
|
||||
|
||||
private fun migrationHelper(databaseName: String): MigrationTestHelper {
|
||||
databaseNames += databaseName
|
||||
return MigrationTestHelper(
|
||||
instrumentation,
|
||||
context.getDatabasePath(databaseName),
|
||||
AndroidSQLiteDriver(),
|
||||
AppDb::class,
|
||||
)
|
||||
}
|
||||
|
||||
private fun openDatabase(databaseName: String): AppDb =
|
||||
Room.databaseBuilder(
|
||||
context,
|
||||
AppDb::class.java,
|
||||
context.getDatabasePath(databaseName).absolutePath,
|
||||
)
|
||||
.setDriver(AndroidSQLiteDriver())
|
||||
.setQueryCoroutineContext(Dispatchers.IO)
|
||||
.build()
|
||||
|
||||
private fun SQLiteConnection.queryLong(sql: String): Long =
|
||||
prepare(sql).use { statement ->
|
||||
check(statement.step()) { "Query returned no rows: $sql" }
|
||||
statement.getLong(0)
|
||||
}
|
||||
|
||||
private fun SQLiteConnection.queryText(sql: String): String =
|
||||
prepare(sql).use { statement ->
|
||||
check(statement.step()) { "Query returned no rows: $sql" }
|
||||
statement.getText(0)
|
||||
}
|
||||
|
||||
@After
|
||||
fun deleteDatabases() {
|
||||
databaseNames.forEach(context::deleteDatabase)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun everyExportedSchemaMigratesToVersion14() = runBlocking {
|
||||
// Protects the persisted schema compatibility contract for every released version.
|
||||
for (startVersion in 1 until 14) {
|
||||
val helper = migrationHelper("all-migrations-$startVersion.db")
|
||||
helper.createDatabase(startVersion).close()
|
||||
helper.runMigrationsAndValidate(14, emptyList()).close()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun migration9To10PreservesRenamedForeignIds() = runBlocking {
|
||||
val helper = migrationHelper("migration-9-10.db")
|
||||
helper.createDatabase(9).apply {
|
||||
execSQL(
|
||||
"""
|
||||
INSERT INTO subs_config
|
||||
(id, type, enable, subs_item_id, app_id, group_key, exclude)
|
||||
VALUES (101, 2, 1, 77, 'sample.app', 5, '')
|
||||
""".trimIndent()
|
||||
)
|
||||
execSQL(
|
||||
"""
|
||||
INSERT INTO category_config
|
||||
(id, enable, subs_item_id, category_key)
|
||||
VALUES (102, 1, 88, 6)
|
||||
""".trimIndent()
|
||||
)
|
||||
close()
|
||||
}
|
||||
|
||||
helper.runMigrationsAndValidate(10, emptyList()).use { connection ->
|
||||
assertEquals(
|
||||
77L,
|
||||
connection.queryLong("SELECT subs_id FROM subs_config WHERE id = 101"),
|
||||
)
|
||||
assertEquals(
|
||||
88L,
|
||||
connection.queryLong("SELECT subs_id FROM category_config WHERE id = 102"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun migration10To11PreservesSnapshotDataOutsideDeletedColumns() = runBlocking {
|
||||
val helper = migrationHelper("migration-10-11.db")
|
||||
helper.createDatabase(10).apply {
|
||||
execSQL(
|
||||
"""
|
||||
INSERT INTO snapshot
|
||||
(id, app_id, activity_id, app_name, app_version_code,
|
||||
app_version_name, screen_height, screen_width, is_landscape,
|
||||
github_asset_id)
|
||||
VALUES
|
||||
(201, 'sample.app', 'sample.Activity', 'Old name', 12,
|
||||
'1.2', 1920, 1080, 0, 301)
|
||||
""".trimIndent()
|
||||
)
|
||||
close()
|
||||
}
|
||||
|
||||
helper.runMigrationsAndValidate(11, emptyList()).use { connection ->
|
||||
assertEquals(
|
||||
"sample.app",
|
||||
connection.queryText("SELECT app_id FROM snapshot WHERE id = 201"),
|
||||
)
|
||||
assertEquals(
|
||||
1920L,
|
||||
connection.queryLong("SELECT screen_height FROM snapshot WHERE id = 201"),
|
||||
)
|
||||
assertEquals(
|
||||
301L,
|
||||
connection.queryLong("SELECT github_asset_id FROM snapshot WHERE id = 201"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun driverDatabaseOpensVersion14AndRollsBackFailedTransaction() = runBlocking {
|
||||
val databaseName = "driver-version-14.db"
|
||||
val helper = migrationHelper(databaseName)
|
||||
helper.createDatabase(14).apply {
|
||||
execSQL(
|
||||
"""
|
||||
INSERT INTO subs_item
|
||||
(id, ctime, mtime, enable, enable_update, `order`, update_url)
|
||||
VALUES (42, 1, 1, 1, 1, 0, NULL)
|
||||
""".trimIndent()
|
||||
)
|
||||
close()
|
||||
}
|
||||
|
||||
val database = openDatabase(databaseName)
|
||||
try {
|
||||
assertEquals(listOf(42L), database.subsItemDao().queryAll().map { it.id })
|
||||
|
||||
var failed = false
|
||||
try {
|
||||
database.useWriterConnection { connection ->
|
||||
connection.immediateTransaction {
|
||||
database.subsItemDao().insert(SubsItem(id = 43, order = 1))
|
||||
error("rollback")
|
||||
}
|
||||
}
|
||||
} catch (_: IllegalStateException) {
|
||||
failed = true
|
||||
}
|
||||
|
||||
assertTrue(failed)
|
||||
assertEquals(listOf(42L), database.subsItemDao().queryAll().map { it.id })
|
||||
} finally {
|
||||
database.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,11 @@ import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverter
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.room.immediateTransaction
|
||||
import androidx.room.migration.AutoMigrationSpec
|
||||
import androidx.room.withTransaction
|
||||
import androidx.room.useWriterConnection
|
||||
import androidx.sqlite.driver.AndroidSQLiteDriver
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
@Database(
|
||||
@@ -116,7 +119,10 @@ object Db {
|
||||
applicationContext,
|
||||
AppDb::class.java,
|
||||
databasePath,
|
||||
).build()
|
||||
)
|
||||
.setDriver(AndroidSQLiteDriver())
|
||||
.setQueryCoroutineContext(Dispatchers.IO)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,5 +141,9 @@ object Db {
|
||||
val a11yEventLogDao get() = database.a11yEventLogDao()
|
||||
|
||||
suspend fun <T> withTransaction(block: suspend () -> T): T =
|
||||
database.withTransaction(block)
|
||||
database.useWriterConnection { connection ->
|
||||
connection.immediateTransaction {
|
||||
block()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ data class SubsItem(
|
||||
fun query(): Flow<List<SubsItem>>
|
||||
|
||||
@Query("SELECT * FROM subs_item ORDER BY `order`")
|
||||
fun queryAll(): List<SubsItem>
|
||||
suspend fun queryAll(): List<SubsItem>
|
||||
|
||||
@Query("DELETE FROM subs_item WHERE id IN (:ids)")
|
||||
suspend fun deleteById(vararg ids: Long): Int
|
||||
|
||||
@@ -6,6 +6,7 @@ agp = "9.3.2"
|
||||
compose = "1.12.0"
|
||||
nav3 = "1.1.6"
|
||||
room = "2.8.4"
|
||||
sqlite = "2.7.0"
|
||||
paging = "3.5.1"
|
||||
lifecycle = "2.11.0"
|
||||
ktor = "3.5.2"
|
||||
@@ -50,12 +51,15 @@ androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecy
|
||||
androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "nav3" }
|
||||
androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "nav3" }
|
||||
androidx-junit = "androidx.test.ext:junit:1.3.0"
|
||||
androidx-test-runner = "androidx.test:runner:1.7.0"
|
||||
androidx-annotation = "androidx.annotation:annotation:1.10.0"
|
||||
androidx-espresso = "androidx.test.espresso:espresso-core:3.7.0"
|
||||
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
|
||||
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
|
||||
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
|
||||
androidx-room-paging = { module = "androidx.room:room-paging", version.ref = "room" }
|
||||
androidx-room-testing = { module = "androidx.room:room-testing", version.ref = "room" }
|
||||
androidx-sqlite-framework = { module = "androidx.sqlite:sqlite-framework", version.ref = "sqlite" }
|
||||
androidx-splashscreen = "androidx.core:core-splashscreen:1.2.0"
|
||||
androidx-paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "paging" }
|
||||
androidx-paging-compose = { module = "androidx.paging:paging-compose", version.ref = "paging" }
|
||||
|
||||
Reference in New Issue
Block a user