Add JDBC database provider for JVM

This commit is contained in:
sergeych committed 2026-04-16 22:10:36 +03:00
1 parent ee392daa13
commit 64273ac60a
22 files changed
+1913 -459

No files matched your search

@@ -0,0 +1,456 @@
/*
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package net.sergeych.lyng.io.db.jdbc
import net.sergeych.lyng.ExecutionError
import net.sergeych.lyng.ScopeFacade
import net.sergeych.lyng.io.db.SqlColumnMeta
import net.sergeych.lyng.io.db.SqlCoreModule
import net.sergeych.lyng.io.db.SqlDatabaseBackend
import net.sergeych.lyng.io.db.SqlExecutionResultData
import net.sergeych.lyng.io.db.SqlResultSetData
import net.sergeych.lyng.io.db.SqlTransactionBackend
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjBool
import net.sergeych.lyng.obj.ObjBuffer
import net.sergeych.lyng.obj.ObjDate
import net.sergeych.lyng.obj.ObjDateTime
import net.sergeych.lyng.obj.ObjEnumEntry
import net.sergeych.lyng.obj.ObjException
import net.sergeych.lyng.obj.ObjInstant
import net.sergeych.lyng.obj.ObjInt
import net.sergeych.lyng.obj.ObjNull
import net.sergeych.lyng.obj.ObjReal
import net.sergeych.lyng.obj.ObjString
import net.sergeych.lyng.requireScope
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant
import java.math.BigDecimal
import java.sql.Connection
import java.sql.DriverManager
import java.sql.PreparedStatement
import java.sql.ResultSet
import java.sql.SQLException
import java.sql.SQLIntegrityConstraintViolationException
import java.sql.SQLNonTransientConnectionException
import java.sql.Statement
import java.util.Properties
import kotlin.time.Instant
private val knownJdbcDrivers = listOf(
"org.sqlite.JDBC",
"org.h2.Driver",
"org.postgresql.Driver",
)
internal actual suspend fun openJdbcBackend(
scope: ScopeFacade,
core: SqlCoreModule,
options: JdbcOpenOptions,
): SqlDatabaseBackend {
return JdbcDatabaseBackend(core, options)
}
private class JdbcDatabaseBackend(
private val core: SqlCoreModule,
private val options: JdbcOpenOptions,
) : SqlDatabaseBackend {
override suspend fun <T> transaction(scope: ScopeFacade, block: suspend (SqlTransactionBackend) -> T): T {
val connection = openConnection(scope)
try {
connection.autoCommit = false
val tx = JdbcTransactionBackend(core, connection)
val result = try {
block(tx)
} catch (e: Throwable) {
throw finishFailedTransaction(scope, core, e) {
rollbackOrThrow(scope, core, connection)
}
}
try {
connection.commit()
} catch (e: SQLException) {
throw mapSqlException(scope, core, e)
}
return result
} catch (e: SQLException) {
throw mapSqlException(scope, core, e)
} finally {
try {
connection.close()
} catch (_: SQLException) {
}
}
}
private fun openConnection(scope: ScopeFacade): Connection {
ensureJdbcDriversLoaded(scope, core, options.driverClass)
val properties = Properties()
options.user?.let { properties.setProperty("user", it) }
options.password?.let { properties.setProperty("password", it) }
options.properties.forEach { (key, value) -> properties.setProperty(key, value) }
return try {
DriverManager.getConnection(options.connectionUrl, properties)
} catch (e: SQLException) {
throw mapOpenException(scope, core, e)
}
}
}
private class JdbcTransactionBackend(
private val core: SqlCoreModule,
private val connection: Connection,
) : SqlTransactionBackend {
override suspend fun select(scope: ScopeFacade, clause: String, params: List<Obj>): SqlResultSetData {
try {
connection.prepareStatement(clause).use { statement ->
bindParams(statement, params, scope, core)
statement.executeQuery().use { rs ->
return readResultSet(scope, core, rs)
}
}
} catch (e: SQLException) {
throw mapSqlException(scope, core, e)
}
}
override suspend fun execute(scope: ScopeFacade, clause: String, params: List<Obj>): SqlExecutionResultData {
if (containsRowReturningClause(clause)) {
scope.raiseError(
ObjException(
core.sqlUsageException,
scope.requireScope(),
ObjString("execute(...) cannot be used with statements that return rows; use select(...)")
)
)
}
try {
connection.prepareStatement(clause, Statement.RETURN_GENERATED_KEYS).use { statement ->
bindParams(statement, params, scope, core)
val hasResultSet = statement.execute()
if (hasResultSet) {
scope.raiseError(
ObjException(
core.sqlUsageException,
scope.requireScope(),
ObjString("execute(...) cannot be used with statements that return rows; use select(...)")
)
)
}
val generatedKeys = statement.generatedKeys.use { rs ->
if (rs == null) emptyResultSet() else readResultSet(scope, core, rs)
}
return SqlExecutionResultData(statement.updateCount, generatedKeys)
}
} catch (e: SQLException) {
throw mapSqlException(scope, core, e)
}
}
override suspend fun <T> transaction(scope: ScopeFacade, block: suspend (SqlTransactionBackend) -> T): T {
val savepoint = try {
connection.setSavepoint()
} catch (e: SQLException) {
throw mapSqlUsage(scope, core, "Nested transactions are not supported by this JDBC backend", e)
}
val nested = JdbcTransactionBackend(core, connection)
val result = try {
block(nested)
} catch (e: Throwable) {
throw finishFailedTransaction(scope, core, e) {
rollbackToSavepointOrThrow(scope, core, connection, savepoint)
releaseSavepointOrThrow(scope, core, connection, savepoint)
}
}
try {
connection.releaseSavepoint(savepoint)
} catch (e: SQLException) {
throw mapSqlException(scope, core, e)
}
return result
}
}
private suspend fun bindParams(statement: PreparedStatement, params: List<Obj>, scope: ScopeFacade, core: SqlCoreModule) {
params.forEachIndexed { index, value ->
val jdbcIndex = index + 1
when (value) {
ObjNull -> statement.setObject(jdbcIndex, null)
is ObjBool -> statement.setBoolean(jdbcIndex, value.value)
is ObjInt -> statement.setLong(jdbcIndex, value.value)
is ObjReal -> statement.setDouble(jdbcIndex, value.value)
is ObjString -> statement.setString(jdbcIndex, value.value)
is ObjBuffer -> statement.setBytes(jdbcIndex, value.byteArray.toByteArray())
is ObjDate -> statement.setObject(jdbcIndex, java.time.LocalDate.parse(value.date.toString()))
is ObjDateTime -> statement.setObject(jdbcIndex, java.time.LocalDateTime.parse(value.localDateTime.toString()))
is ObjInstant -> statement.setObject(jdbcIndex, java.time.Instant.parse(value.instant.toString()))
else -> when (value.objClass.className) {
"Decimal" -> statement.setBigDecimal(jdbcIndex, BigDecimal(scope.toStringOf(value).value))
else -> scope.raiseError(
ObjException(
core.sqlUsageException,
scope.requireScope(),
ObjString("Unsupported JDBC parameter type: ${value.objClass.className}")
)
)
}
}
}
}
private suspend fun readResultSet(scope: ScopeFacade, core: SqlCoreModule, resultSet: ResultSet): SqlResultSetData {
val meta = resultSet.metaData
val columns = (1..meta.columnCount).map { index ->
val nativeType = meta.getColumnTypeName(index) ?: ""
SqlColumnMeta(
name = meta.getColumnLabel(index),
sqlType = mapSqlType(core, nativeType, meta.getColumnType(index)),
nullable = meta.isNullable(index) != java.sql.ResultSetMetaData.columnNoNulls,
nativeType = nativeType,
)
}
val rows = mutableListOf<List<Obj>>()
while (resultSet.next()) {
rows += columns.mapIndexed { index, column ->
readColumnValue(scope, core, resultSet, index + 1, column.nativeType)
}
}
return SqlResultSetData(columns, rows)
}
private suspend fun readColumnValue(
scope: ScopeFacade,
core: SqlCoreModule,
resultSet: ResultSet,
index: Int,
nativeType: String,
): Obj {
val value = resultSet.getObject(index) ?: return ObjNull
val normalizedNativeType = normalizeDeclaredTypeName(nativeType)
return when (value) {
is Boolean -> ObjBool(value)
is Byte, is Short, is Int -> ObjInt.of((value as Number).toLong())
is Long -> ObjInt.of(value)
is Float, is Double -> ObjReal.of((value as Number).toDouble())
is java.math.BigInteger -> ObjInt.of(value.longValueExact())
is BigDecimal -> decimalFromString(scope, value.toPlainString())
is ByteArray -> ObjBuffer(value.toUByteArray())
is java.sql.Date -> ObjDate(LocalDate.parse(value.toLocalDate().toString()))
is java.sql.Timestamp -> timestampValue(scope, normalizedNativeType, value.toInstant())
is java.time.LocalDate -> ObjDate(LocalDate.parse(value.toString()))
is java.time.LocalDateTime -> ObjDateTime(value.toString().let(LocalDateTime::parse).toInstant(TimeZone.UTC), TimeZone.UTC)
is java.time.OffsetDateTime -> ObjInstant(Instant.parse(value.toInstant().toString()))
is java.time.ZonedDateTime -> ObjInstant(Instant.parse(value.toInstant().toString()))
is java.time.Instant -> ObjInstant(Instant.parse(value.toString()))
is java.sql.Time -> ObjString(value.toLocalTime().toString())
is java.time.LocalTime -> ObjString(value.toString())
is java.time.OffsetTime -> ObjString(value.toString())
is String -> stringValue(scope, normalizedNativeType, value)
else -> ObjString(value.toString())
}
}
private suspend fun stringValue(scope: ScopeFacade, normalizedNativeType: String, value: String): Obj {
return when {
normalizedNativeType == "DATE" -> ObjDate(LocalDate.parse(value.trim()))
normalizedNativeType == "TIMESTAMP WITH TIME ZONE" || normalizedNativeType == "TIMESTAMPTZ" ->
ObjInstant(Instant.parse(value.trim()))
normalizedNativeType == "TIMESTAMP" || normalizedNativeType == "DATETIME" ->
ObjDateTime(LocalDateTime.parse(value.trim()).toInstant(TimeZone.UTC), TimeZone.UTC)
normalizedNativeType == "DECIMAL" || normalizedNativeType == "NUMERIC" -> decimalFromString(scope, value.trim())
else -> ObjString(value)
}
}
private fun timestampValue(scope: ScopeFacade, normalizedNativeType: String, value: java.time.Instant): Obj {
return if (normalizedNativeType == "TIMESTAMP WITH TIME ZONE" || normalizedNativeType == "TIMESTAMPTZ") {
ObjInstant(Instant.parse(value.toString()))
} else {
val local = value.atOffset(java.time.ZoneOffset.UTC).toLocalDateTime().toString()
ObjDateTime(LocalDateTime.parse(local).toInstant(TimeZone.UTC), TimeZone.UTC)
}
}
private suspend fun decimalFromString(scope: ScopeFacade, value: String): Obj {
val decimalModule = scope.requireScope().currentImportProvider.createModuleScope(scope.pos, "lyng.decimal")
val decimalClass = decimalModule.requireClass("Decimal")
return decimalClass.invokeInstanceMethod(scope.requireScope(), "fromString", ObjString(value))
}
private fun normalizeDeclaredTypeName(nativeTypeName: String): String {
val strippedSuffix = nativeTypeName.trim().replace(Regex("""\s*\(.*\)\s*$"""), "")
return strippedSuffix.uppercase().replace(Regex("""\s+"""), " ").trim()
}
private fun mapSqlType(core: SqlCoreModule, nativeTypeName: String, jdbcType: Int): ObjEnumEntry {
val normalized = normalizeDeclaredTypeName(nativeTypeName)
return when {
normalized == "BOOLEAN" || normalized == "BOOL" -> core.sqlTypes.require("Bool")
normalized == "DATE" -> core.sqlTypes.require("Date")
normalized == "TIMESTAMP" || normalized == "DATETIME" -> core.sqlTypes.require("DateTime")
normalized == "TIMESTAMP WITH TIME ZONE" || normalized == "TIMESTAMPTZ" -> core.sqlTypes.require("Instant")
normalized == "TIME" || normalized == "TIME WITHOUT TIME ZONE" || normalized == "TIME WITH TIME ZONE" -> core.sqlTypes.require("String")
normalized == "DECIMAL" || normalized == "NUMERIC" -> core.sqlTypes.require("Decimal")
normalized.contains("BLOB") || normalized.contains("BINARY") || normalized == "BYTEA" -> core.sqlTypes.require("Binary")
normalized.contains("INT") -> core.sqlTypes.require("Int")
normalized.contains("CHAR") || normalized.contains("TEXT") || normalized.contains("CLOB") || normalized == "VARCHAR" -> core.sqlTypes.require("String")
normalized.contains("REAL") || normalized.contains("FLOA") || normalized.contains("DOUB") -> core.sqlTypes.require("Double")
jdbcType == java.sql.Types.BOOLEAN || jdbcType == java.sql.Types.BIT -> core.sqlTypes.require("Bool")
jdbcType == java.sql.Types.DATE -> core.sqlTypes.require("Date")
jdbcType == java.sql.Types.TIMESTAMP -> core.sqlTypes.require("DateTime")
jdbcType == java.sql.Types.TIMESTAMP_WITH_TIMEZONE -> core.sqlTypes.require("Instant")
jdbcType == java.sql.Types.TIME || jdbcType == java.sql.Types.TIME_WITH_TIMEZONE -> core.sqlTypes.require("String")
jdbcType == java.sql.Types.BLOB || jdbcType == java.sql.Types.BINARY || jdbcType == java.sql.Types.VARBINARY || jdbcType == java.sql.Types.LONGVARBINARY -> core.sqlTypes.require("Binary")
jdbcType == java.sql.Types.INTEGER || jdbcType == java.sql.Types.BIGINT || jdbcType == java.sql.Types.SMALLINT || jdbcType == java.sql.Types.TINYINT -> core.sqlTypes.require("Int")
jdbcType == java.sql.Types.DECIMAL || jdbcType == java.sql.Types.NUMERIC -> core.sqlTypes.require("Decimal")
jdbcType == java.sql.Types.FLOAT || jdbcType == java.sql.Types.REAL || jdbcType == java.sql.Types.DOUBLE -> core.sqlTypes.require("Double")
else -> core.sqlTypes.require("String")
}
}
private fun emptyResultSet(): SqlResultSetData = SqlResultSetData(emptyList(), emptyList())
private fun containsRowReturningClause(clause: String): Boolean =
Regex("""\b(returning|output)\b""", RegexOption.IGNORE_CASE).containsMatchIn(clause)
private fun ensureJdbcDriversLoaded(scope: ScopeFacade, core: SqlCoreModule, requestedDriverClass: String?) {
for (driverClass in knownJdbcDrivers) {
try {
Class.forName(driverClass)
} catch (_: Throwable) {
}
}
val explicit = requestedDriverClass ?: return
try {
Class.forName(explicit)
} catch (e: ClassNotFoundException) {
throw ExecutionError(
ObjException(core.databaseException, scope.requireScope(), ObjString("JDBC driver class not found: $explicit")),
scope.pos,
"JDBC driver class not found: $explicit",
e,
)
}
}
private fun rollbackOrThrow(scope: ScopeFacade, core: SqlCoreModule, connection: Connection) {
try {
connection.rollback()
} catch (e: SQLException) {
throw mapSqlException(scope, core, e)
}
}
private fun rollbackToSavepointOrThrow(
scope: ScopeFacade,
core: SqlCoreModule,
connection: Connection,
savepoint: java.sql.Savepoint,
) {
try {
connection.rollback(savepoint)
} catch (e: SQLException) {
throw mapSqlException(scope, core, e)
}
}
private fun releaseSavepointOrThrow(
scope: ScopeFacade,
core: SqlCoreModule,
connection: Connection,
savepoint: java.sql.Savepoint,
) {
try {
connection.releaseSavepoint(savepoint)
} catch (e: SQLException) {
throw mapSqlException(scope, core, e)
}
}
private inline fun finishFailedTransaction(
scope: ScopeFacade,
core: SqlCoreModule,
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: SqlCoreModule): 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)
}
private fun mapOpenException(scope: ScopeFacade, core: SqlCoreModule, e: SQLException): Nothing {
val message = e.message ?: "JDBC open failed"
if (e is SQLNonTransientConnectionException) {
throw ExecutionError(
ObjException(core.databaseException, scope.requireScope(), ObjString(message)),
scope.pos,
message,
e,
)
}
throw ExecutionError(
ObjException(core.databaseException, scope.requireScope(), ObjString(message)),
scope.pos,
message,
e,
)
}
private fun mapSqlException(scope: ScopeFacade, core: SqlCoreModule, e: SQLException): ExecutionError {
val exceptionClass = when {
e is SQLIntegrityConstraintViolationException -> core.sqlConstraintException
e.sqlState?.startsWith("23") == true -> core.sqlConstraintException
else -> core.sqlExecutionException
}
return ExecutionError(
ObjException(exceptionClass, scope.requireScope(), ObjString(e.message ?: "JDBC error")),
scope.pos,
e.message ?: "JDBC error",
e,
)
}
private fun mapSqlUsage(scope: ScopeFacade, core: SqlCoreModule, message: String, cause: Throwable? = null): ExecutionError {
return ExecutionError(
ObjException(core.sqlUsageException, scope.requireScope(), ObjString(message)),
scope.pos,
message,
cause,
)
}
@@ -20,6 +20,12 @@ package net.sergeych.lyng.io.db.sqlite
import net.sergeych.lyng.ExecutionError
import net.sergeych.lyng.Arguments
import net.sergeych.lyng.ScopeFacade
import net.sergeych.lyng.io.db.SqlColumnMeta
import net.sergeych.lyng.io.db.SqlCoreModule
import net.sergeych.lyng.io.db.SqlDatabaseBackend
import net.sergeych.lyng.io.db.SqlExecutionResultData
import net.sergeych.lyng.io.db.SqlResultSetData
import net.sergeych.lyng.io.db.SqlTransactionBackend
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjBool
import net.sergeych.lyng.obj.ObjBuffer
@@ -50,24 +56,24 @@ import kotlin.time.Instant
internal actual suspend fun openSqliteBackend(
scope: ScopeFacade,
core: SqliteCoreModule,
core: SqlCoreModule,
options: SqliteOpenOptions,
): SqliteDatabaseBackend {
): SqlDatabaseBackend {
if (options.busyTimeoutMillis < 0) {
scope.raiseIllegalArgument("busyTimeoutMillis must be >= 0")
}
return JdbcSqliteDatabaseBackend(core, options)
return JdbcSqlDatabaseBackend(core, options)
}
private class JdbcSqliteDatabaseBackend(
private val core: SqliteCoreModule,
private class JdbcSqlDatabaseBackend(
private val core: SqlCoreModule,
private val options: SqliteOpenOptions,
) : SqliteDatabaseBackend {
override suspend fun <T> transaction(scope: ScopeFacade, block: suspend (SqliteTransactionBackend) -> T): T {
) : SqlDatabaseBackend {
override suspend fun <T> transaction(scope: ScopeFacade, block: suspend (SqlTransactionBackend) -> T): T {
val connection = openConnection(scope)
try {
connection.autoCommit = false
val tx = JdbcSqliteTransactionBackend(core, connection)
val tx = JdbcSqlTransactionBackend(core, connection)
val result = try {
block(tx)
} catch (e: Throwable) {
@@ -124,11 +130,11 @@ private class JdbcSqliteDatabaseBackend(
}
}
private class JdbcSqliteTransactionBackend(
private val core: SqliteCoreModule,
private class JdbcSqlTransactionBackend(
private val core: SqlCoreModule,
private val connection: Connection,
) : SqliteTransactionBackend {
override suspend fun select(scope: ScopeFacade, clause: String, params: List<Obj>): SqliteResultSetData {
) : SqlTransactionBackend {
override suspend fun select(scope: ScopeFacade, clause: String, params: List<Obj>): SqlResultSetData {
try {
connection.prepareStatement(clause).use { statement ->
bindParams(statement, params, scope, core)
@@ -141,7 +147,7 @@ private class JdbcSqliteTransactionBackend(
}
}
override suspend fun execute(scope: ScopeFacade, clause: String, params: List<Obj>): SqliteExecutionResultData {
override suspend fun execute(scope: ScopeFacade, clause: String, params: List<Obj>): SqlExecutionResultData {
if (containsRowReturningClause(clause)) {
scope.raiseError(
ObjException(
@@ -172,20 +178,20 @@ private class JdbcSqliteTransactionBackend(
readResultSet(scope, core, rs)
}
}
return SqliteExecutionResultData(affected, generatedKeys)
return SqlExecutionResultData(affected, generatedKeys)
}
} catch (e: SQLException) {
throw mapSqlException(scope, core, e)
}
}
override suspend fun <T> transaction(scope: ScopeFacade, block: suspend (SqliteTransactionBackend) -> T): T {
override suspend fun <T> transaction(scope: ScopeFacade, block: suspend (SqlTransactionBackend) -> T): T {
val savepoint = try {
connection.setSavepoint()
} catch (e: SQLException) {
throw mapSqlUsage(scope, core, "Nested transactions are not supported by this SQLite backend", e)
}
val nested = JdbcSqliteTransactionBackend(core, connection)
val nested = JdbcSqlTransactionBackend(core, connection)
val result = try {
block(nested)
} catch (e: Throwable) {
@@ -203,7 +209,7 @@ private class JdbcSqliteTransactionBackend(
}
}
private suspend fun bindParams(statement: PreparedStatement, params: List<Obj>, scope: ScopeFacade, core: SqliteCoreModule) {
private suspend fun bindParams(statement: PreparedStatement, params: List<Obj>, scope: ScopeFacade, core: SqlCoreModule) {
params.forEachIndexed { index, value ->
val jdbcIndex = index + 1
when (value) {
@@ -231,12 +237,12 @@ private suspend fun bindParams(statement: PreparedStatement, params: List<Obj>,
private suspend fun readResultSet(
scope: ScopeFacade,
core: SqliteCoreModule,
core: SqlCoreModule,
resultSet: ResultSet,
): SqliteResultSetData {
): SqlResultSetData {
val meta = resultSet.metaData
val columns = (1..meta.columnCount).map { index ->
SqliteColumnMeta(
SqlColumnMeta(
name = meta.getColumnLabel(index),
sqlType = mapSqlType(core, meta.getColumnTypeName(index), meta.getColumnType(index)),
nullable = meta.isNullable(index) != java.sql.ResultSetMetaData.columnNoNulls,
@@ -249,12 +255,12 @@ private suspend fun readResultSet(
readColumnValue(scope, core, resultSet, index + 1, column.nativeType)
}
}
return SqliteResultSetData(columns, rows)
return SqlResultSetData(columns, rows)
}
private suspend fun readColumnValue(
scope: ScopeFacade,
core: SqliteCoreModule,
core: SqlCoreModule,
resultSet: ResultSet,
index: Int,
nativeType: String,
@@ -278,7 +284,7 @@ private suspend fun readColumnValue(
private fun convertIntegerValue(
scope: ScopeFacade,
core: SqliteCoreModule,
core: SqlCoreModule,
normalizedNativeType: String,
value: Long,
): Obj {
@@ -294,7 +300,7 @@ private fun convertIntegerValue(
private suspend fun convertStringValue(
scope: ScopeFacade,
core: SqliteCoreModule,
core: SqlCoreModule,
normalizedNativeType: String,
value: String,
): Obj {
@@ -317,7 +323,7 @@ private fun isDecimalNativeType(normalizedNativeType: String): Boolean =
private fun isBooleanNativeType(normalizedNativeType: String): Boolean =
normalizedNativeType == "BOOLEAN" || normalizedNativeType == "BOOL"
private fun booleanFromString(scope: ScopeFacade, core: SqliteCoreModule, value: String): Obj {
private fun booleanFromString(scope: ScopeFacade, core: SqlCoreModule, value: String): Obj {
return when (value.trim().lowercase()) {
"true", "t" -> ObjBool(true)
"false", "f" -> ObjBool(false)
@@ -331,7 +337,7 @@ private suspend fun decimalFromString(scope: ScopeFacade, value: String): Obj {
return decimalClass.invokeInstanceMethod(scope.requireScope(), "fromString", ObjString(value))
}
private fun dateTimeFromString(scope: ScopeFacade, core: SqliteCoreModule, value: String): ObjDateTime {
private fun dateTimeFromString(scope: ScopeFacade, core: SqlCoreModule, value: String): ObjDateTime {
val trimmed = value.trim()
if (hasExplicitTimeZone(trimmed)) {
sqlExecutionFailure(scope, core, "SQLite TIMESTAMP/DATETIME value must not contain a timezone offset: $value")
@@ -358,7 +364,7 @@ private fun normalizeDeclaredTypeName(nativeTypeName: String): String {
return strippedSuffix.uppercase().replace(Regex("""\s+"""), " ").trim()
}
private fun mapSqlType(core: SqliteCoreModule, nativeTypeName: String, jdbcType: Int): ObjEnumEntry {
private fun mapSqlType(core: SqlCoreModule, nativeTypeName: String, jdbcType: Int): ObjEnumEntry {
val normalized = normalizeDeclaredTypeName(nativeTypeName)
return when {
normalized == "BOOLEAN" || normalized == "BOOL" -> core.sqlTypes.require("Bool")
@@ -385,9 +391,9 @@ private fun mapSqlType(core: SqliteCoreModule, nativeTypeName: String, jdbcType:
}
}
private fun emptyResultSet(core: SqliteCoreModule): SqliteResultSetData = SqliteResultSetData(emptyList(), emptyList())
private fun emptyResultSet(core: SqlCoreModule): SqlResultSetData = SqlResultSetData(emptyList(), emptyList())
private fun sqlExecutionFailure(scope: ScopeFacade, core: SqliteCoreModule, message: String): Nothing {
private fun sqlExecutionFailure(scope: ScopeFacade, core: SqlCoreModule, message: String): Nothing {
throw ExecutionError(
ObjException(core.sqlExecutionException, scope.requireScope(), ObjString(message)),
scope.pos,
@@ -395,7 +401,7 @@ private fun sqlExecutionFailure(scope: ScopeFacade, core: SqliteCoreModule, mess
)
}
private fun rollbackOrThrow(scope: ScopeFacade, core: SqliteCoreModule, connection: Connection) {
private fun rollbackOrThrow(scope: ScopeFacade, core: SqlCoreModule, connection: Connection) {
try {
connection.rollback()
} catch (e: SQLException) {
@@ -405,7 +411,7 @@ private fun rollbackOrThrow(scope: ScopeFacade, core: SqliteCoreModule, connecti
private fun rollbackToSavepointOrThrow(
scope: ScopeFacade,
core: SqliteCoreModule,
core: SqlCoreModule,
connection: Connection,
savepoint: java.sql.Savepoint,
) {
@@ -418,7 +424,7 @@ private fun rollbackToSavepointOrThrow(
private fun releaseSavepointOrThrow(
scope: ScopeFacade,
core: SqliteCoreModule,
core: SqlCoreModule,
connection: Connection,
savepoint: java.sql.Savepoint,
) {
@@ -431,7 +437,7 @@ private fun releaseSavepointOrThrow(
private inline fun finishFailedTransaction(
scope: ScopeFacade,
core: SqliteCoreModule,
core: SqlCoreModule,
failure: Throwable,
rollback: () -> Unit,
): Throwable {
@@ -449,7 +455,7 @@ private inline fun finishFailedTransaction(
}
}
private fun isRollbackSignal(failure: Throwable, core: SqliteCoreModule): Boolean {
private fun isRollbackSignal(failure: Throwable, core: SqlCoreModule): Boolean {
val errorObject = (failure as? ExecutionError)?.errorObject ?: return false
return errorObject.isInstanceOf(core.rollbackException)
}
@@ -459,7 +465,7 @@ private fun attachSecondaryFailure(primary: Throwable, secondary: Throwable) {
primary.addSuppressed(secondary)
}
private fun mapOpenException(scope: ScopeFacade, core: SqliteCoreModule, e: SQLException): Nothing {
private fun mapOpenException(scope: ScopeFacade, core: SqlCoreModule, e: SQLException): Nothing {
val message = e.message ?: "SQLite open failed"
val lower = message.lowercase()
if ("malformed" in lower || "no such access mode" in lower || "invalid uri" in lower) {
@@ -473,7 +479,7 @@ private fun mapOpenException(scope: ScopeFacade, core: SqliteCoreModule, e: SQLE
)
}
private fun mapSqlException(scope: ScopeFacade, core: SqliteCoreModule, e: SQLException): ExecutionError {
private fun mapSqlException(scope: ScopeFacade, core: SqlCoreModule, e: SQLException): ExecutionError {
val code = SQLiteErrorCode.getErrorCode(e.errorCode)
val exceptionClass = when (code) {
SQLiteErrorCode.SQLITE_CONSTRAINT,
@@ -491,7 +497,7 @@ private fun mapSqlException(scope: ScopeFacade, core: SqliteCoreModule, e: SQLEx
)
}
private fun mapSqlUsage(scope: ScopeFacade, core: SqliteCoreModule, message: String, cause: Throwable? = null): ExecutionError {
private fun mapSqlUsage(scope: ScopeFacade, core: SqlCoreModule, message: String, cause: Throwable? = null): ExecutionError {
return ExecutionError(
ObjException(core.sqlUsageException, scope.requireScope(), ObjString(message)),
scope.pos,