improved JDBC provider for lyng.io.db

This commit is contained in:
Sergey Chernov 2026-04-18 02:06:29 +03:00
parent 49fc700233
commit 30e56946a0
5 changed files with 65 additions and 50 deletions

View File

@ -19,6 +19,7 @@ sqlite-jdbc = "3.50.3.0"
h2 = "2.4.240"
postgresql = "42.7.8"
testcontainers = "1.20.6"
hikaricp = "6.2.1"
[libraries]
clikt = { module = "com.github.ajalt.clikt:clikt", version.ref = "clikt" }
@ -52,6 +53,7 @@ h2 = { module = "com.h2database:h2", version.ref = "h2" }
postgresql = { module = "org.postgresql:postgresql", version.ref = "postgresql" }
testcontainers = { module = "org.testcontainers:testcontainers", version.ref = "testcontainers" }
testcontainers-postgresql = { module = "org.testcontainers:postgresql", version.ref = "testcontainers" }
hikaricp = { module = "com.zaxxer:HikariCP", version.ref = "hikaricp" }
[plugins]
androidLibrary = { id = "com.android.library", version.ref = "agp" }

View File

@ -19,7 +19,6 @@
* LyngIO: Compose Multiplatform library module depending on :lynglib
*/
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
@ -225,6 +224,7 @@ kotlin {
implementation(libs.sqlite.jdbc)
implementation(libs.h2)
implementation(libs.postgresql)
implementation(libs.hikaricp)
}
}
// // For Wasm we use in-memory VFS for now

View File

@ -33,6 +33,21 @@ import net.sergeych.lyng.obj.ObjNull
import net.sergeych.lyng.obj.ObjString
import net.sergeych.lyng.obj.thisAs
import net.sergeych.lyng.requireScope
import kotlin.collections.List
import kotlin.collections.Map
import kotlin.collections.MutableList
import kotlin.collections.associateWith
import kotlin.collections.drop
import kotlin.collections.first
import kotlin.collections.forEachIndexed
import kotlin.collections.getOrNull
import kotlin.collections.getOrPut
import kotlin.collections.indices
import kotlin.collections.linkedMapOf
import kotlin.collections.listOf
import kotlin.collections.map
import kotlin.collections.mutableListOf
import kotlin.text.lowercase
internal data class SqlColumnMeta(
val name: String,
@ -53,6 +68,7 @@ internal data class SqlExecutionResultData(
internal interface SqlDatabaseBackend {
suspend fun <T> transaction(scope: ScopeFacade, block: suspend (SqlTransactionBackend) -> T): T
fun close() {}
}
internal interface SqlTransactionBackend {
@ -156,6 +172,11 @@ internal class SqlRuntimeTypes private constructor(
}
private fun bind() {
databaseClass.addFn("close") {
thisAs<SqlDatabaseObj>().backend.close()
ObjNull
}
databaseClass.addFn("transaction") {
val self = thisAs<SqlDatabaseObj>()
val block = args.list.getOrNull(0) ?: raiseError("Expected exactly 1 argument, got ${args.list.size}")

View File

@ -17,41 +17,19 @@
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 com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant
import net.sergeych.lyng.ExecutionError
import net.sergeych.lyng.ScopeFacade
import net.sergeych.lyng.io.db.*
import net.sergeych.lyng.obj.*
import net.sergeych.lyng.requireScope
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 java.sql.*
import kotlin.time.Instant
private val knownJdbcDrivers = listOf(
@ -65,17 +43,42 @@ internal actual suspend fun openJdbcBackend(
core: SqlCoreModule,
options: JdbcOpenOptions,
): SqlDatabaseBackend {
return JdbcDatabaseBackend(core, options)
ensureJdbcDriversLoaded(scope, core, options.driverClass)
val dataSource = try {
val config = HikariConfig().apply {
jdbcUrl = options.connectionUrl
options.user?.let { username = it }
options.password?.let { password = it }
options.driverClass?.let { driverClassName = it }
options.properties.forEach { (key, value) -> addDataSourceProperty(key, value) }
// Statement caching: avoids re-parsing identical SQL on every call
addDataSourceProperty("cachePrepStmts", "true")
addDataSourceProperty("prepStmtCacheSize", "250")
addDataSourceProperty("prepStmtCacheSqlLimit", "2048")
// Connections from the pool are already in autoCommit=false territory;
// we manage commits explicitly in transaction().
isAutoCommit = false
}
HikariDataSource(config)
} catch (e: Exception) {
val cause = e.cause as? SQLException ?: (e as? SQLException)
if (cause != null) throw mapOpenException(scope, core, cause)
throw e
}
return JdbcDatabaseBackend(core, dataSource)
}
private class JdbcDatabaseBackend(
private val core: SqlCoreModule,
private val options: JdbcOpenOptions,
private val dataSource: HikariDataSource,
) : SqlDatabaseBackend {
override suspend fun <T> transaction(scope: ScopeFacade, block: suspend (SqlTransactionBackend) -> T): T {
val connection = openConnection(scope)
val connection = try {
dataSource.connection
} catch (e: SQLException) {
throw mapOpenException(scope, core, e)
}
try {
connection.autoCommit = false
val tx = JdbcTransactionBackend(core, connection)
val result = try {
block(tx)
@ -90,27 +93,16 @@ private class JdbcDatabaseBackend(
throw mapSqlException(scope, core, e)
}
return result
} catch (e: SQLException) {
throw mapSqlException(scope, core, e)
} finally {
try {
connection.close()
connection.close() // returns connection to pool
} 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)
}
override fun close() {
dataSource.close()
}
}

View File

@ -236,7 +236,7 @@ suspend fun runDocTests(fileName: String, bookMode: Boolean = false) {
val bookScope = Scope()
var count = 0
parseDocTests(fileName, bookMode).collect { dt ->
if (bookMode) dt.test(bookScope)
if (bookMode)imp dt.test(bookScope)
else dt.test()
count++
}