Add JDBC database provider for JVM
This commit is contained in:
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,
|
||||
|
||||
Reference in new issue
Block a user