From 30e56946a011081fa38aa1c20f1c78e8a5c58e8a Mon Sep 17 00:00:00 2001 From: sergeych Date: Sat, 18 Apr 2026 02:06:29 +0300 Subject: [PATCH] improved JDBC provider for lyng.io.db --- gradle/libs.versions.toml | 2 + lyngio/build.gradle.kts | 2 +- .../sergeych/lyng/io/db/SqlRuntimeSupport.kt | 21 +++++ .../sergeych/lyng/io/db/jdbc/PlatformJvm.kt | 88 +++++++++---------- lynglib/src/jvmTest/kotlin/BookTest.kt | 2 +- 5 files changed, 65 insertions(+), 50 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a25b160..39200f7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" } diff --git a/lyngio/build.gradle.kts b/lyngio/build.gradle.kts index 6ceb22b..84a1c96 100644 --- a/lyngio/build.gradle.kts +++ b/lyngio/build.gradle.kts @@ -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 diff --git a/lyngio/src/commonMain/kotlin/net/sergeych/lyng/io/db/SqlRuntimeSupport.kt b/lyngio/src/commonMain/kotlin/net/sergeych/lyng/io/db/SqlRuntimeSupport.kt index d0d558a..a3c0757 100644 --- a/lyngio/src/commonMain/kotlin/net/sergeych/lyng/io/db/SqlRuntimeSupport.kt +++ b/lyngio/src/commonMain/kotlin/net/sergeych/lyng/io/db/SqlRuntimeSupport.kt @@ -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 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().backend.close() + ObjNull + } + databaseClass.addFn("transaction") { val self = thisAs() val block = args.list.getOrNull(0) ?: raiseError("Expected exactly 1 argument, got ${args.list.size}") diff --git a/lyngio/src/jvmMain/kotlin/net/sergeych/lyng/io/db/jdbc/PlatformJvm.kt b/lyngio/src/jvmMain/kotlin/net/sergeych/lyng/io/db/jdbc/PlatformJvm.kt index 630714a..5f055a4 100644 --- a/lyngio/src/jvmMain/kotlin/net/sergeych/lyng/io/db/jdbc/PlatformJvm.kt +++ b/lyngio/src/jvmMain/kotlin/net/sergeych/lyng/io/db/jdbc/PlatformJvm.kt @@ -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 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() } } diff --git a/lynglib/src/jvmTest/kotlin/BookTest.kt b/lynglib/src/jvmTest/kotlin/BookTest.kt index ddacdac..c4d48e5 100644 --- a/lynglib/src/jvmTest/kotlin/BookTest.kt +++ b/lynglib/src/jvmTest/kotlin/BookTest.kt @@ -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++ }