diff --git a/lyngio/src/jvmTest/kotlin/net/sergeych/lyng/io/db/sqlite/LyngSqliteModuleTest.kt b/lyngio/src/jvmTest/kotlin/net/sergeych/lyng/io/db/sqlite/LyngSqliteModuleTest.kt index 3fe7902..9961d94 100644 --- a/lyngio/src/jvmTest/kotlin/net/sergeych/lyng/io/db/sqlite/LyngSqliteModuleTest.kt +++ b/lyngio/src/jvmTest/kotlin/net/sergeych/lyng/io/db/sqlite/LyngSqliteModuleTest.kt @@ -33,6 +33,7 @@ import net.sergeych.lyng.obj.ObjInstant import net.sergeych.lyng.obj.ObjMap import net.sergeych.lyng.obj.ObjNull import net.sergeych.lyng.obj.ObjString +import net.sergeych.lyng.obj.raiseAsExecutionError import net.sergeych.lyng.obj.requiredArg import net.sergeych.lyng.requireScope import kotlinx.datetime.TimeZone @@ -134,6 +135,56 @@ class LyngSqliteModuleTest { assertEquals(1L, count.value) } + @Test + fun testRollbackExceptionRollsBackAndPropagates() = runTest { + val scope = Script.newScope() + withTempDb(scope) { db -> + db.invokeInstanceMethod( + scope, + "transaction", + ObjExternCallable.fromBridge { + val tx = requiredArg(0) + tx.invokeInstanceMethod( + requireScope(), + "execute", + ObjString("create table items(id integer primary key autoincrement, name text not null)") + ) + } + ) + + val error = assertFailsWith { + db.invokeInstanceMethod( + scope, + "transaction", + ObjExternCallable.fromBridge { + val tx = requiredArg(0) + tx.invokeInstanceMethod( + requireScope(), + "execute", + ObjString("insert into items(name) values(?)"), + ObjString("rolled-back") + ) + rollbackException(requireScope(), "stop here").raiseAsExecutionError(requireScope()) + } + ) + } + + assertEquals("RollbackException", error.errorObject.objClass.className) + + val count = db.invokeInstanceMethod( + scope, + "transaction", + ObjExternCallable.fromBridge { + val tx = requiredArg(0) + val resultSet = tx.invokeInstanceMethod(requireScope(), "select", ObjString("select count(*) as count from items")) + rowsOf(requireScope(), resultSet)[0].getAt(requireScope(), ObjString("count")) + } + ) as ObjInt + + assertEquals(0L, count.value) + } + } + @Test fun testResultSetFailsAfterTransactionEnds() = runTest { val scope = Script.newScope() @@ -543,6 +594,65 @@ class LyngSqliteModuleTest { } } + @Test + fun testCommitFailureBecomesPrimaryAfterNormalCompletion() = runTest { + val scope = Script.newScope() + val db = openMemoryDb(scope) + + val error = assertFailsWith { + db.invokeInstanceMethod( + scope, + "transaction", + ObjExternCallable.fromBridge { + val tx = requiredArg(0) + tx.invokeInstanceMethod(requireScope(), "execute", ObjString("rollback")) + } + ) + } + + assertEquals("SqlExecutionException", error.errorObject.objClass.className) + } + + @Test + fun testUserExceptionStaysPrimaryWhenRollbackFails() = runTest { + val scope = Script.newScope() + val db = openMemoryDb(scope) + + val error = assertFailsWith { + db.invokeInstanceMethod( + scope, + "transaction", + ObjExternCallable.fromBridge { + val tx = requiredArg(0) + tx.invokeInstanceMethod(requireScope(), "execute", ObjString("rollback")) + throw IllegalStateException("boom") + } + ) + } + + assertEquals("boom", error.message) + } + + @Test + fun testRollbackFailureBecomesPrimaryAfterRollbackException() = runTest { + val scope = Script.newScope() + val db = openMemoryDb(scope) + + val error = assertFailsWith { + db.invokeInstanceMethod( + scope, + "transaction", + ObjExternCallable.fromBridge { + val tx = requiredArg(0) + tx.invokeInstanceMethod(requireScope(), "execute", ObjString("rollback")) + rollbackException(requireScope(), "rollback requested").raiseAsExecutionError(requireScope()) + } + ) + } + + assertEquals("SqlExecutionException", error.errorObject.objClass.className) + } + private suspend fun ModuleScope.callFn(name: String, vararg args: Obj): Obj { val callee = get(name)?.value ?: error("Missing $name in module") return callee.invoke(this, ObjNull, *args) @@ -587,6 +697,12 @@ class LyngSqliteModuleTest { return decimalClass.invokeInstanceMethod(scope, "fromString", ObjString(value)) } + private suspend fun rollbackException(scope: Scope, message: String): net.sergeych.lyng.obj.ObjException { + val dbModule = scope.currentImportProvider.createModuleScope(scope.pos, "lyng.io.db") + val rollbackClass = dbModule.requireClass("RollbackException") + return rollbackClass.invoke(scope, ObjNull, ObjString(message)) as net.sergeych.lyng.obj.ObjException + } + private suspend fun dateOf(scope: Scope, value: String): Obj { val timeModule = scope.currentImportProvider.createModuleScope(scope.pos, "lyng.time") val dateClass = timeModule.requireClass("Date") diff --git a/lyngio/src/linuxTest/kotlin/net/sergeych/lyng/io/db/sqlite/LyngSqliteModuleNativeTest.kt b/lyngio/src/linuxTest/kotlin/net/sergeych/lyng/io/db/sqlite/LyngSqliteModuleNativeTest.kt index d44e961..2365dda 100644 --- a/lyngio/src/linuxTest/kotlin/net/sergeych/lyng/io/db/sqlite/LyngSqliteModuleNativeTest.kt +++ b/lyngio/src/linuxTest/kotlin/net/sergeych/lyng/io/db/sqlite/LyngSqliteModuleNativeTest.kt @@ -34,6 +34,7 @@ import net.sergeych.lyng.obj.ObjInstant import net.sergeych.lyng.obj.ObjMap import net.sergeych.lyng.obj.ObjNull import net.sergeych.lyng.obj.ObjString +import net.sergeych.lyng.obj.raiseAsExecutionError import net.sergeych.lyng.obj.requiredArg import net.sergeych.lyng.requireScope import okio.FileSystem @@ -109,6 +110,56 @@ class LyngSqliteModuleNativeTest { assertEquals(1L, count.value) } + @Test + fun testRollbackExceptionRollsBackAndPropagates() = runTest { + val scope = Script.newScope() + withTempDb(scope) { db -> + db.invokeInstanceMethod( + scope, + "transaction", + ObjExternCallable.fromBridge { + val tx = requiredArg(0) + tx.invokeInstanceMethod( + requireScope(), + "execute", + ObjString("create table items(id integer primary key autoincrement, name text not null)") + ) + } + ) + + val error = assertFailsWith { + db.invokeInstanceMethod( + scope, + "transaction", + ObjExternCallable.fromBridge { + val tx = requiredArg(0) + tx.invokeInstanceMethod( + requireScope(), + "execute", + ObjString("insert into items(name) values(?)"), + ObjString("rolled-back") + ) + rollbackException(requireScope(), "stop here").raiseAsExecutionError(requireScope()) + } + ) + } + + assertEquals("RollbackException", error.errorObject.objClass.className) + + val count = db.invokeInstanceMethod( + scope, + "transaction", + ObjExternCallable.fromBridge { + val tx = requiredArg(0) + val resultSet = tx.invokeInstanceMethod(requireScope(), "select", ObjString("select count(*) as count from items")) + rowsOf(requireScope(), resultSet)[0].getAt(requireScope(), ObjString("count")) + } + ) as ObjInt + + assertEquals(0L, count.value) + } + } + @Test fun testResultSetFailsAfterTransactionEnds() = runTest { val scope = Script.newScope() @@ -438,6 +489,65 @@ class LyngSqliteModuleNativeTest { } } + @Test + fun testCommitFailureBecomesPrimaryAfterNormalCompletion() = runTest { + val scope = Script.newScope() + val db = openMemoryDb(scope) + + val error = assertFailsWith { + db.invokeInstanceMethod( + scope, + "transaction", + ObjExternCallable.fromBridge { + val tx = requiredArg(0) + tx.invokeInstanceMethod(requireScope(), "execute", ObjString("rollback")) + } + ) + } + + assertEquals("SqlExecutionException", error.errorObject.objClass.className) + } + + @Test + fun testUserExceptionStaysPrimaryWhenRollbackFails() = runTest { + val scope = Script.newScope() + val db = openMemoryDb(scope) + + val error = assertFailsWith { + db.invokeInstanceMethod( + scope, + "transaction", + ObjExternCallable.fromBridge { + val tx = requiredArg(0) + tx.invokeInstanceMethod(requireScope(), "execute", ObjString("rollback")) + throw IllegalStateException("boom") + } + ) + } + + assertEquals("boom", error.message) + } + + @Test + fun testRollbackFailureBecomesPrimaryAfterRollbackException() = runTest { + val scope = Script.newScope() + val db = openMemoryDb(scope) + + val error = assertFailsWith { + db.invokeInstanceMethod( + scope, + "transaction", + ObjExternCallable.fromBridge { + val tx = requiredArg(0) + tx.invokeInstanceMethod(requireScope(), "execute", ObjString("rollback")) + rollbackException(requireScope(), "rollback requested").raiseAsExecutionError(requireScope()) + } + ) + } + + assertEquals("SqlExecutionException", error.errorObject.objClass.className) + } + private suspend fun ModuleScope.callFn(name: String, vararg args: Obj): Obj { val callee = get(name)?.value ?: error("Missing $name in module") return callee.invoke(this, ObjNull, *args) @@ -488,6 +598,12 @@ class LyngSqliteModuleNativeTest { return decimalClass.invokeInstanceMethod(scope, "fromString", ObjString(value)) } + private suspend fun rollbackException(scope: Scope, message: String): net.sergeych.lyng.obj.ObjException { + val dbModule = scope.currentImportProvider.createModuleScope(scope.pos, "lyng.io.db") + val rollbackClass = dbModule.requireClass("RollbackException") + return rollbackClass.invoke(scope, ObjNull, ObjString(message)) as net.sergeych.lyng.obj.ObjException + } + private suspend fun dateOf(scope: Scope, value: String): Obj { val timeModule = scope.currentImportProvider.createModuleScope(scope.pos, "lyng.time") val dateClass = timeModule.requireClass("Date")