Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.artemchep.keyguard.core.store

import app.cash.sqldelight.TransacterImpl
import app.cash.sqldelight.db.AfterVersion
import app.cash.sqldelight.db.QueryResult
import app.cash.sqldelight.db.SqlDriver
Expand Down Expand Up @@ -68,32 +69,11 @@ class DatabaseSqlManagerInFileJvm<Database>(
file = file,
key = masterKey.byteArray,
)

// Create or migrate the database schema.
val targetVersion = databaseSchema.version
val currentVersion = runCatching {
driver.getCurrentVersion()
}.getOrDefault(0L)
if (currentVersion > targetVersion) {
try {
migrateDatabaseSchema(driver, databaseSchema, *callbacks)
} catch (e: Throwable) {
driver.close()
throw DatabaseSchemaDowngradeException(
currentVersion = currentVersion,
targetVersion = targetVersion,
)
}
if (currentVersion == 0L) {
databaseSchema.create(driver)
} else if (targetVersion > currentVersion) {
databaseSchema.migrate(
driver,
currentVersion,
targetVersion,
*callbacks,
)
}
// Bump the version to the current one.
if (currentVersion < targetVersion) {
driver.setCurrentVersion(targetVersion)
throw e
}

val database = databaseFactory(driver)
Expand Down Expand Up @@ -147,28 +127,63 @@ class DatabaseSqlManagerInFileJvm<Database>(
)
}

// Version

private suspend fun SqlDriver.getCurrentVersion(): Long {
val queryResult = executeQuery(
identifier = null,
sql = "PRAGMA user_version;",
mapper = { cursor ->
val version = cursor.getLong(0)
requireNotNull(version)
}

QueryResult.Value(version)
},
parameters = 0,
binders = null,
)
return queryResult.await()
internal fun migrateDatabaseSchema(
driver: SqlDriver,
databaseSchema: SqlSchema<QueryResult.Value<Unit>>,
vararg callbacks: AfterVersion,
) {
// DDL is transactional in SQLite. Keeping schema changes and the
// user_version bump in one transaction prevents an interrupted upgrade
// from replaying already-created tables on the next run.
val transacter = object : TransacterImpl(driver) {}
transacter.transaction {
val targetVersion = databaseSchema.version
val currentVersion = runCatching {
driver.getCurrentVersion()
}.getOrDefault(0L)
if (currentVersion > targetVersion) {
throw DatabaseSchemaDowngradeException(
currentVersion = currentVersion,
targetVersion = targetVersion,
)
}
if (currentVersion == 0L) {
databaseSchema.create(driver)
} else if (targetVersion > currentVersion) {
databaseSchema.migrate(
driver,
currentVersion,
targetVersion,
*callbacks,
)
}
if (currentVersion < targetVersion) {
driver.setCurrentVersion(targetVersion)
}
}
}

private suspend fun SqlDriver.setCurrentVersion(version: Long) {
execute(null, "PRAGMA user_version = $version;", 0, null)
.await()
}
private fun SqlDriver.getCurrentVersion(): Long {
val queryResult = executeQuery(
identifier = null,
sql = "PRAGMA user_version;",
mapper = { cursor ->
val version = cursor.getLong(0)
requireNotNull(version)

QueryResult.Value(version)
},
parameters = 0,
binders = null,
)
return queryResult.value
}

private fun SqlDriver.setCurrentVersion(version: Long) {
execute(null, "PRAGMA user_version = $version;", 0, null)
.value
}

internal class DatabaseSchemaDowngradeException(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.artemchep.keyguard.core.store

import app.cash.sqldelight.db.AfterVersion
import app.cash.sqldelight.db.QueryResult
import app.cash.sqldelight.db.SqlDriver
import app.cash.sqldelight.db.SqlSchema
import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue

class DatabaseSqlManagerInFileJvmTest {
@Test
fun `interrupted schema migration rolls back before retry`() {
var attempts = 0
val schema = object : SqlSchema<QueryResult.Value<Unit>> {
override val version: Long = 2L

override fun create(driver: SqlDriver) = QueryResult.Value(Unit)

override fun migrate(
driver: SqlDriver,
oldVersion: Long,
newVersion: Long,
vararg callbacks: AfterVersion,
): QueryResult.Value<Unit> {
assertEquals(1L, oldVersion)
assertEquals(2L, newVersion)
driver.execute(null, "CREATE TABLE example (id INTEGER PRIMARY KEY)", 0, null)
attempts += 1
if (attempts == 1) {
error("simulated interruption")
}
return QueryResult.Value(Unit)
}
}
val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)

try {
driver.execute(null, "PRAGMA user_version = 1", 0, null).value
val firstAttempt = runCatching {
migrateDatabaseSchema(driver, schema)
}
assertTrue(firstAttempt.isFailure)
migrateDatabaseSchema(driver, schema)

assertEquals(2, attempts)
val count = driver.executeQuery(
identifier = null,
sql = "SELECT COUNT(*) FROM example",
mapper = { cursor ->
check(cursor.next().value)
QueryResult.Value(cursor.getLong(0))
},
parameters = 0,
binders = null,
).value
assertEquals(0L, count)
} finally {
driver.close()
}
}
}
Loading