Harden SQLite provider release behavior
This commit is contained in:
parent
f9bbdd56bf
commit
14dc73db3e
@ -465,7 +465,12 @@ private fun mapOpenException(scope: ScopeFacade, core: SqliteCoreModule, e: SQLE
|
||||
if ("malformed" in lower || "no such access mode" in lower || "invalid uri" in lower) {
|
||||
scope.raiseIllegalArgument(message)
|
||||
}
|
||||
throw mapSqlException(scope, core, e)
|
||||
throw ExecutionError(
|
||||
ObjException(core.databaseException, scope.requireScope(), ObjString(message)),
|
||||
scope.pos,
|
||||
message,
|
||||
e,
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapSqlException(scope: ScopeFacade, core: SqliteCoreModule, e: SQLException): ExecutionError {
|
||||
|
||||
@ -212,6 +212,30 @@ class LyngSqliteModuleTest {
|
||||
assertTrue(error.errorMessage.contains("transaction is active"), error.errorMessage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRowFailsAfterTransactionEnds() = runTest {
|
||||
val scope = Script.newScope()
|
||||
val db = openMemoryDb(scope)
|
||||
var leakedRow: Obj = ObjNull
|
||||
|
||||
db.invokeInstanceMethod(
|
||||
scope,
|
||||
"transaction",
|
||||
ObjExternCallable.fromBridge {
|
||||
val tx = requiredArg<Obj>(0)
|
||||
leakedRow = rowsOf(requireScope(), tx.invokeInstanceMethod(requireScope(), "select", ObjString("select 42 as answer")))[0]
|
||||
ObjNull
|
||||
}
|
||||
)
|
||||
|
||||
val error = assertFailsWith<ExecutionError> {
|
||||
leakedRow.getAt(scope, ObjString("answer"))
|
||||
}
|
||||
|
||||
assertEquals("SqlUsageException", error.errorObject.objClass.className)
|
||||
assertTrue(error.errorMessage.contains("transaction is active"), error.errorMessage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInvalidSqliteUrlFailsWithIllegalArgument() = runTest {
|
||||
val scope = Script.newScope()
|
||||
@ -594,6 +618,151 @@ class LyngSqliteModuleTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMissingFileWithCreateIfMissingFalseFailsWithDatabaseException() = runTest {
|
||||
val scope = Script.newScope()
|
||||
createSqliteModule(scope.importManager)
|
||||
val sqliteModule = scope.importManager.createModuleScope(Pos.builtIn, "lyng.io.db.sqlite")
|
||||
val missingDir = Files.createTempDirectory("lyng-sqlite-missing-dir-")
|
||||
missingDir.deleteIfExists()
|
||||
val missingFile = missingDir.resolve("missing.db")
|
||||
|
||||
try {
|
||||
val db = sqliteModule.callFn(
|
||||
"openSqlite",
|
||||
ObjString(missingFile.toString()),
|
||||
net.sergeych.lyng.obj.ObjFalse,
|
||||
net.sergeych.lyng.obj.ObjFalse
|
||||
)
|
||||
|
||||
val error = assertFailsWith<ExecutionError> {
|
||||
db.invokeInstanceMethod(
|
||||
scope,
|
||||
"transaction",
|
||||
ObjExternCallable.fromBridge { ObjNull }
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals("DatabaseException", error.errorObject.objClass.className)
|
||||
} finally {
|
||||
missingFile.deleteIfExists()
|
||||
missingDir.deleteIfExists()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGenericOpenDatabaseReadOnlyOptionMatchesTypedHelper() = runTest {
|
||||
val scope = Script.newScope()
|
||||
createSqliteModule(scope.importManager)
|
||||
scope.importManager.createModuleScope(Pos.builtIn, "lyng.io.db.sqlite")
|
||||
val dbModule = scope.importManager.createModuleScope(Pos.builtIn, "lyng.io.db")
|
||||
val tempFile = Files.createTempFile("lyng-sqlite-generic-", ".db")
|
||||
|
||||
try {
|
||||
val writableDb = dbModule.callFn("openDatabase", ObjString("sqlite:${tempFile}"), sqliteOptions(scope))
|
||||
writableDb.invokeInstanceMethod(
|
||||
scope,
|
||||
"transaction",
|
||||
ObjExternCallable.fromBridge {
|
||||
val tx = requiredArg<Obj>(0)
|
||||
tx.invokeInstanceMethod(
|
||||
requireScope(),
|
||||
"execute",
|
||||
ObjString("create table item(id integer primary key autoincrement, name text not null)")
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
val readOnlyDb = dbModule.callFn(
|
||||
"openDatabase",
|
||||
ObjString("sqlite:${tempFile}"),
|
||||
sqliteOptions(
|
||||
scope,
|
||||
"readOnly" to net.sergeych.lyng.obj.ObjTrue,
|
||||
"createIfMissing" to net.sergeych.lyng.obj.ObjFalse
|
||||
)
|
||||
)
|
||||
|
||||
val error = assertFailsWith<ExecutionError> {
|
||||
readOnlyDb.invokeInstanceMethod(
|
||||
scope,
|
||||
"transaction",
|
||||
ObjExternCallable.fromBridge {
|
||||
val tx = requiredArg<Obj>(0)
|
||||
tx.invokeInstanceMethod(
|
||||
requireScope(),
|
||||
"execute",
|
||||
ObjString("insert into item(name) values(?)"),
|
||||
ObjString("blocked")
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals("SqlExecutionException", error.errorObject.objClass.className)
|
||||
} finally {
|
||||
tempFile.deleteIfExists()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testForeignKeysOptionControlsConstraintEnforcement() = runTest {
|
||||
val scope = Script.newScope()
|
||||
createSqliteModule(scope.importManager)
|
||||
val sqliteModule = scope.importManager.createModuleScope(Pos.builtIn, "lyng.io.db.sqlite")
|
||||
val tempFile = Files.createTempFile("lyng-sqlite-fk-", ".db")
|
||||
|
||||
try {
|
||||
val dbNoFk = sqliteModule.callFn(
|
||||
"openSqlite",
|
||||
ObjString(tempFile.toString()),
|
||||
net.sergeych.lyng.obj.ObjFalse,
|
||||
net.sergeych.lyng.obj.ObjTrue,
|
||||
net.sergeych.lyng.obj.ObjFalse
|
||||
)
|
||||
dbNoFk.invokeInstanceMethod(
|
||||
scope,
|
||||
"transaction",
|
||||
ObjExternCallable.fromBridge {
|
||||
val tx = requiredArg<Obj>(0)
|
||||
tx.invokeInstanceMethod(requireScope(), "execute", ObjString("create table parent(id integer primary key)"))
|
||||
tx.invokeInstanceMethod(
|
||||
requireScope(),
|
||||
"execute",
|
||||
ObjString("create table child(parent_id integer not null references parent(id))")
|
||||
)
|
||||
tx.invokeInstanceMethod(
|
||||
requireScope(),
|
||||
"execute",
|
||||
ObjString("insert into child(parent_id) values(?)"),
|
||||
ObjInt.One
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
val dbWithFk = sqliteModule.callFn("openSqlite", ObjString(tempFile.toString()))
|
||||
val error = assertFailsWith<ExecutionError> {
|
||||
dbWithFk.invokeInstanceMethod(
|
||||
scope,
|
||||
"transaction",
|
||||
ObjExternCallable.fromBridge {
|
||||
val tx = requiredArg<Obj>(0)
|
||||
tx.invokeInstanceMethod(
|
||||
requireScope(),
|
||||
"execute",
|
||||
ObjString("insert into child(parent_id) values(?)"),
|
||||
ObjInt.of(2)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals("SqlConstraintException", error.errorObject.objClass.className)
|
||||
} finally {
|
||||
tempFile.deleteIfExists()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCommitFailureBecomesPrimaryAfterNormalCompletion() = runTest {
|
||||
val scope = Script.newScope()
|
||||
@ -658,6 +827,14 @@ class LyngSqliteModuleTest {
|
||||
return callee.invoke(this, ObjNull, *args)
|
||||
}
|
||||
|
||||
private suspend fun sqliteOptions(scope: Scope, vararg entries: Pair<String, Obj>): ObjMap {
|
||||
val result = ObjMap()
|
||||
for ((key, value) in entries) {
|
||||
result.putAt(scope, ObjString(key), value)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private suspend fun openMemoryDb(scope: Scope): Obj {
|
||||
createSqliteModule(scope.importManager)
|
||||
val sqliteModule = scope.importManager.createModuleScope(Pos.builtIn, "lyng.io.db.sqlite")
|
||||
|
||||
@ -185,6 +185,30 @@ class LyngSqliteModuleNativeTest {
|
||||
assertTrue(error.errorMessage.contains("transaction is active"), error.errorMessage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRowFailsAfterTransactionEnds() = runTest {
|
||||
val scope = Script.newScope()
|
||||
val db = openMemoryDb(scope)
|
||||
var leakedRow: Obj = ObjNull
|
||||
|
||||
db.invokeInstanceMethod(
|
||||
scope,
|
||||
"transaction",
|
||||
ObjExternCallable.fromBridge {
|
||||
val tx = requiredArg<Obj>(0)
|
||||
leakedRow = rowsOf(requireScope(), tx.invokeInstanceMethod(requireScope(), "select", ObjString("select 42 as answer")))[0]
|
||||
ObjNull
|
||||
}
|
||||
)
|
||||
|
||||
val error = assertFailsWith<ExecutionError> {
|
||||
leakedRow.getAt(scope, ObjString("answer"))
|
||||
}
|
||||
|
||||
assertEquals("SqlUsageException", error.errorObject.objClass.className)
|
||||
assertTrue(error.errorMessage.contains("transaction is active"), error.errorMessage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExecuteRejectsReturningButSelectSupportsIt() = runTest {
|
||||
val scope = Script.newScope()
|
||||
@ -489,6 +513,145 @@ class LyngSqliteModuleNativeTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMissingFileWithCreateIfMissingFalseFailsWithDatabaseException() = runTest {
|
||||
val scope = Script.newScope()
|
||||
createSqliteModule(scope.importManager)
|
||||
val sqliteModule = scope.importManager.createModuleScope(Pos.builtIn, "lyng.io.db.sqlite")
|
||||
val missingDir = "/tmp/lyng-sqlite-missing-${Random.nextInt(Int.MAX_VALUE)}".toPath()
|
||||
val missingFile = (missingDir.toString() + "/missing.db").toPath()
|
||||
try {
|
||||
FileSystem.SYSTEM.delete(missingFile, mustExist = false)
|
||||
FileSystem.SYSTEM.delete(missingDir, mustExist = false)
|
||||
val db = sqliteModule.callFn(
|
||||
"openSqlite",
|
||||
ObjString(missingFile.toString()),
|
||||
net.sergeych.lyng.obj.ObjFalse,
|
||||
net.sergeych.lyng.obj.ObjFalse
|
||||
)
|
||||
|
||||
val error = assertFailsWith<ExecutionError> {
|
||||
db.invokeInstanceMethod(
|
||||
scope,
|
||||
"transaction",
|
||||
ObjExternCallable.fromBridge { ObjNull }
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals("DatabaseException", error.errorObject.objClass.className)
|
||||
} finally {
|
||||
FileSystem.SYSTEM.delete(missingFile, mustExist = false)
|
||||
FileSystem.SYSTEM.delete(missingDir, mustExist = false)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGenericOpenDatabaseReadOnlyOptionMatchesTypedHelper() = runTest {
|
||||
val scope = Script.newScope()
|
||||
createSqliteModule(scope.importManager)
|
||||
scope.importManager.createModuleScope(Pos.builtIn, "lyng.io.db.sqlite")
|
||||
val dbModule = scope.importManager.createModuleScope(Pos.builtIn, "lyng.io.db")
|
||||
|
||||
withTempPath { tempPath ->
|
||||
val writableDb = dbModule.callFn("openDatabase", ObjString("sqlite:${tempPath}"), sqliteOptions(scope))
|
||||
writableDb.invokeInstanceMethod(
|
||||
scope,
|
||||
"transaction",
|
||||
ObjExternCallable.fromBridge {
|
||||
val tx = requiredArg<Obj>(0)
|
||||
tx.invokeInstanceMethod(
|
||||
requireScope(),
|
||||
"execute",
|
||||
ObjString("create table item(id integer primary key autoincrement, name text not null)")
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
val readOnlyDb = dbModule.callFn(
|
||||
"openDatabase",
|
||||
ObjString("sqlite:${tempPath}"),
|
||||
sqliteOptions(
|
||||
scope,
|
||||
"readOnly" to net.sergeych.lyng.obj.ObjTrue,
|
||||
"createIfMissing" to net.sergeych.lyng.obj.ObjFalse
|
||||
)
|
||||
)
|
||||
|
||||
val error = assertFailsWith<ExecutionError> {
|
||||
readOnlyDb.invokeInstanceMethod(
|
||||
scope,
|
||||
"transaction",
|
||||
ObjExternCallable.fromBridge {
|
||||
val tx = requiredArg<Obj>(0)
|
||||
tx.invokeInstanceMethod(
|
||||
requireScope(),
|
||||
"execute",
|
||||
ObjString("insert into item(name) values(?)"),
|
||||
ObjString("blocked")
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals("SqlExecutionException", error.errorObject.objClass.className)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testForeignKeysOptionControlsConstraintEnforcement() = runTest {
|
||||
val scope = Script.newScope()
|
||||
createSqliteModule(scope.importManager)
|
||||
val sqliteModule = scope.importManager.createModuleScope(Pos.builtIn, "lyng.io.db.sqlite")
|
||||
|
||||
withTempPath { tempPath ->
|
||||
val dbNoFk = sqliteModule.callFn(
|
||||
"openSqlite",
|
||||
ObjString(tempPath.toString()),
|
||||
net.sergeych.lyng.obj.ObjFalse,
|
||||
net.sergeych.lyng.obj.ObjTrue,
|
||||
net.sergeych.lyng.obj.ObjFalse
|
||||
)
|
||||
dbNoFk.invokeInstanceMethod(
|
||||
scope,
|
||||
"transaction",
|
||||
ObjExternCallable.fromBridge {
|
||||
val tx = requiredArg<Obj>(0)
|
||||
tx.invokeInstanceMethod(requireScope(), "execute", ObjString("create table parent(id integer primary key)"))
|
||||
tx.invokeInstanceMethod(
|
||||
requireScope(),
|
||||
"execute",
|
||||
ObjString("create table child(parent_id integer not null references parent(id))")
|
||||
)
|
||||
tx.invokeInstanceMethod(
|
||||
requireScope(),
|
||||
"execute",
|
||||
ObjString("insert into child(parent_id) values(?)"),
|
||||
ObjInt.One
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
val dbWithFk = sqliteModule.callFn("openSqlite", ObjString(tempPath.toString()))
|
||||
val error = assertFailsWith<ExecutionError> {
|
||||
dbWithFk.invokeInstanceMethod(
|
||||
scope,
|
||||
"transaction",
|
||||
ObjExternCallable.fromBridge {
|
||||
val tx = requiredArg<Obj>(0)
|
||||
tx.invokeInstanceMethod(
|
||||
requireScope(),
|
||||
"execute",
|
||||
ObjString("insert into child(parent_id) values(?)"),
|
||||
ObjInt.of(2)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals("SqlConstraintException", error.errorObject.objClass.className)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCommitFailureBecomesPrimaryAfterNormalCompletion() = runTest {
|
||||
val scope = Script.newScope()
|
||||
@ -553,6 +716,14 @@ class LyngSqliteModuleNativeTest {
|
||||
return callee.invoke(this, ObjNull, *args)
|
||||
}
|
||||
|
||||
private suspend fun sqliteOptions(scope: Scope, vararg entries: Pair<String, Obj>): ObjMap {
|
||||
val result = ObjMap()
|
||||
for ((key, value) in entries) {
|
||||
result.putAt(scope, ObjString(key), value)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private suspend fun openMemoryDb(scope: Scope): Obj {
|
||||
createSqliteModule(scope.importManager)
|
||||
val sqliteModule = scope.importManager.createModuleScope(Pos.builtIn, "lyng.io.db.sqlite")
|
||||
|
||||
@ -8,6 +8,22 @@ import lyng.io.db
|
||||
Importing this module registers the `sqlite:` URL scheme for
|
||||
`openDatabase(...)`.
|
||||
|
||||
Accepted generic URL forms:
|
||||
- `sqlite::memory:`
|
||||
- `sqlite:relative/path.db`
|
||||
- `sqlite:/absolute/path.db`
|
||||
|
||||
Provider-specific `openDatabase(..., extraParams)` options:
|
||||
- `readOnly: Bool`
|
||||
- `createIfMissing: Bool`
|
||||
- `foreignKeys: Bool`
|
||||
- `busyTimeoutMillis: Int`
|
||||
|
||||
Open-time URL/config validation failures should surface as
|
||||
`IllegalArgumentException`.
|
||||
Runtime SQLite open failures such as opening a missing file with
|
||||
`createIfMissing = false` should surface as `DatabaseException`.
|
||||
|
||||
SQLite provider defaults:
|
||||
- `Bool` is written as `0` / `1`
|
||||
- `Decimal` is written as canonical text
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user