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"
|
||||
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" }
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}")
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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++
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user