improved JDBC provider for lyng.io.db
This commit is contained in:
parent
49fc700233
commit
30e56946a0
@ -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" }
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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}")
|
||||||
|
|||||||
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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++
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user