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) {
|
if ("malformed" in lower || "no such access mode" in lower || "invalid uri" in lower) {
|
||||||
scope.raiseIllegalArgument(message)
|
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 {
|
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)
|
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
|
@Test
|
||||||
fun testInvalidSqliteUrlFailsWithIllegalArgument() = runTest {
|
fun testInvalidSqliteUrlFailsWithIllegalArgument() = runTest {
|
||||||
val scope = Script.newScope()
|
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
|
@Test
|
||||||
fun testCommitFailureBecomesPrimaryAfterNormalCompletion() = runTest {
|
fun testCommitFailureBecomesPrimaryAfterNormalCompletion() = runTest {
|
||||||
val scope = Script.newScope()
|
val scope = Script.newScope()
|
||||||
@ -658,6 +827,14 @@ class LyngSqliteModuleTest {
|
|||||||
return callee.invoke(this, ObjNull, *args)
|
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 {
|
private suspend fun openMemoryDb(scope: Scope): Obj {
|
||||||
createSqliteModule(scope.importManager)
|
createSqliteModule(scope.importManager)
|
||||||
val sqliteModule = scope.importManager.createModuleScope(Pos.builtIn, "lyng.io.db.sqlite")
|
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)
|
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
|
@Test
|
||||||
fun testExecuteRejectsReturningButSelectSupportsIt() = runTest {
|
fun testExecuteRejectsReturningButSelectSupportsIt() = runTest {
|
||||||
val scope = Script.newScope()
|
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
|
@Test
|
||||||
fun testCommitFailureBecomesPrimaryAfterNormalCompletion() = runTest {
|
fun testCommitFailureBecomesPrimaryAfterNormalCompletion() = runTest {
|
||||||
val scope = Script.newScope()
|
val scope = Script.newScope()
|
||||||
@ -553,6 +716,14 @@ class LyngSqliteModuleNativeTest {
|
|||||||
return callee.invoke(this, ObjNull, *args)
|
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 {
|
private suspend fun openMemoryDb(scope: Scope): Obj {
|
||||||
createSqliteModule(scope.importManager)
|
createSqliteModule(scope.importManager)
|
||||||
val sqliteModule = scope.importManager.createModuleScope(Pos.builtIn, "lyng.io.db.sqlite")
|
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
|
Importing this module registers the `sqlite:` URL scheme for
|
||||||
`openDatabase(...)`.
|
`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:
|
SQLite provider defaults:
|
||||||
- `Bool` is written as `0` / `1`
|
- `Bool` is written as `0` / `1`
|
||||||
- `Decimal` is written as canonical text
|
- `Decimal` is written as canonical text
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user