Tighten SQLite transaction failures and tests

This commit is contained in:
sergeych committed 2026-04-15 22:20:29 +03:00
1 parent 5f6f6b9ae4
commit 6d340824e4
4 files changed
+294 -39

No files matched your search

@@ -119,14 +119,15 @@ private class NativeSqliteDatabaseBackend(
try {
handle.execUnit(scope, core, "begin")
val tx = NativeSqliteTransactionBackend(core, handle, savepoints)
return try {
val result = block(tx)
handle.execUnit(scope, core, "commit")
result
val result = try {
block(tx)
} catch (e: Throwable) {
handle.execUnitQuietly("rollback")
throw e
throw finishFailedTransaction(scope, core, e) {
handle.execUnit(scope, core, "rollback")
}
}
handle.execUnit(scope, core, "commit")
return result
} finally {
handle.close()
}
@@ -149,15 +150,17 @@ private class NativeSqliteTransactionBackend(
override suspend fun <T> transaction(scope: ScopeFacade, block: suspend (SqliteTransactionBackend) -> T): T {
val savepoint = "lyng_sp_${savepoints.next()}"
handle.execUnit(scope, core, "savepoint $savepoint")
return try {
val result = block(NativeSqliteTransactionBackend(core, handle, savepoints))
handle.execUnit(scope, core, "release savepoint $savepoint")
result
val nested = NativeSqliteTransactionBackend(core, handle, savepoints)
val result = try {
block(nested)
} catch (e: Throwable) {
handle.execUnitQuietly("rollback to savepoint $savepoint")
handle.execUnitQuietly("release savepoint $savepoint")
throw e
throw finishFailedTransaction(scope, core, e) {
handle.execUnit(scope, core, "rollback to savepoint $savepoint")
handle.execUnit(scope, core, "release savepoint $savepoint")
}
}
handle.execUnit(scope, core, "release savepoint $savepoint")
return result
}
}
@@ -229,17 +232,6 @@ private class NativeSqliteHandle(
}
}
fun execUnitQuietly(sql: String) {
memScoped {
val stmt = lyng_sqlite3_prepare(db, sql) ?: return@memScoped
try {
sqlite3_step(stmt)
} finally {
sqlite3_finalize(stmt)
}
}
}
fun close() {
sqlite3_close_v2(db)
}
@@ -600,3 +592,33 @@ private fun sqlExecutionError(scope: ScopeFacade, core: SqliteCoreModule, messag
message,
)
}
private inline fun finishFailedTransaction(
scope: ScopeFacade,
core: SqliteCoreModule,
failure: Throwable,
rollback: () -> Unit,
): Throwable {
return try {
rollback()
failure
} catch (rollbackFailure: Throwable) {
if (isRollbackSignal(failure, core)) {
attachSecondaryFailure(rollbackFailure, failure)
rollbackFailure
} else {
attachSecondaryFailure(failure, rollbackFailure)
failure
}
}
}
private fun isRollbackSignal(failure: Throwable, core: SqliteCoreModule): Boolean {
val errorObject = (failure as? ExecutionError)?.errorObject ?: return false
return errorObject.isInstanceOf(core.rollbackException)
}
private fun attachSecondaryFailure(primary: Throwable, secondary: Throwable) {
if (primary === secondary) return
primary.addSuppressed(secondary)
}