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" h2 = "2.4.240"
postgresql = "42.7.8" postgresql = "42.7.8"
testcontainers = "1.20.6" testcontainers = "1.20.6"
hikaricp = "6.2.1"
[libraries] [libraries]
clikt = { module = "com.github.ajalt.clikt:clikt", version.ref = "clikt" } 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" } postgresql = { module = "org.postgresql:postgresql", version.ref = "postgresql" }
testcontainers = { module = "org.testcontainers:testcontainers", version.ref = "testcontainers" } testcontainers = { module = "org.testcontainers:testcontainers", version.ref = "testcontainers" }
testcontainers-postgresql = { module = "org.testcontainers:postgresql", version.ref = "testcontainers" } testcontainers-postgresql = { module = "org.testcontainers:postgresql", version.ref = "testcontainers" }
hikaricp = { module = "com.zaxxer:HikariCP", version.ref = "hikaricp" }
[plugins] [plugins]
androidLibrary = { id = "com.android.library", version.ref = "agp" } androidLibrary = { id = "com.android.library", version.ref = "agp" }

View File

@ -19,7 +19,6 @@
* LyngIO: Compose Multiplatform library module depending on :lynglib * LyngIO: Compose Multiplatform library module depending on :lynglib
*/ */
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.JvmTarget
@ -225,6 +224,7 @@ kotlin {
implementation(libs.sqlite.jdbc) implementation(libs.sqlite.jdbc)
implementation(libs.h2) implementation(libs.h2)
implementation(libs.postgresql) implementation(libs.postgresql)
implementation(libs.hikaricp)
} }
} }
// // For Wasm we use in-memory VFS for now // // 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.ObjString
import net.sergeych.lyng.obj.thisAs import net.sergeych.lyng.obj.thisAs
import net.sergeych.lyng.requireScope 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( internal data class SqlColumnMeta(
val name: String, val name: String,
@ -53,6 +68,7 @@ internal data class SqlExecutionResultData(
internal interface SqlDatabaseBackend { internal interface SqlDatabaseBackend {
suspend fun <T> transaction(scope: ScopeFacade, block: suspend (SqlTransactionBackend) -> T): T suspend fun <T> transaction(scope: ScopeFacade, block: suspend (SqlTransactionBackend) -> T): T
fun close() {}
} }
internal interface SqlTransactionBackend { internal interface SqlTransactionBackend {
@ -156,6 +172,11 @@ internal class SqlRuntimeTypes private constructor(
} }
private fun bind() { private fun bind() {
databaseClass.addFn("close") {
thisAs<SqlDatabaseObj>().backend.close()
ObjNull
}
databaseClass.addFn("transaction") { databaseClass.addFn("transaction") {
val self = thisAs<SqlDatabaseObj>() val self = thisAs<SqlDatabaseObj>()
val block = args.list.getOrNull(0) ?: raiseError("Expected exactly 1 argument, got ${args.list.size}") 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 package net.sergeych.lyng.io.db.jdbc
import net.sergeych.lyng.ExecutionError import com.zaxxer.hikari.HikariConfig
import net.sergeych.lyng.ScopeFacade import com.zaxxer.hikari.HikariDataSource
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.LocalDate
import kotlinx.datetime.LocalDateTime import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant 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.math.BigDecimal
import java.sql.Connection import java.sql.*
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 import kotlin.time.Instant
private val knownJdbcDrivers = listOf( private val knownJdbcDrivers = listOf(
@ -65,17 +43,42 @@ internal actual suspend fun openJdbcBackend(
core: SqlCoreModule, core: SqlCoreModule,
options: JdbcOpenOptions, options: JdbcOpenOptions,
): SqlDatabaseBackend { ): 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 class JdbcDatabaseBackend(
private val core: SqlCoreModule, private val core: SqlCoreModule,
private val options: JdbcOpenOptions, private val dataSource: HikariDataSource,
) : SqlDatabaseBackend { ) : SqlDatabaseBackend {
override suspend fun <T> transaction(scope: ScopeFacade, block: suspend (SqlTransactionBackend) -> T): T { 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 { try {
connection.autoCommit = false
val tx = JdbcTransactionBackend(core, connection) val tx = JdbcTransactionBackend(core, connection)
val result = try { val result = try {
block(tx) block(tx)
@ -90,27 +93,16 @@ private class JdbcDatabaseBackend(
throw mapSqlException(scope, core, e) throw mapSqlException(scope, core, e)
} }
return result return result
} catch (e: SQLException) {
throw mapSqlException(scope, core, e)
} finally { } finally {
try { try {
connection.close() connection.close() // returns connection to pool
} catch (_: SQLException) { } catch (_: SQLException) {
} }
} }
} }
private fun openConnection(scope: ScopeFacade): Connection { override fun close() {
ensureJdbcDriversLoaded(scope, core, options.driverClass) dataSource.close()
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)
}
} }
} }

View File

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