Upgrade Kotlin to 2.1.0 and implement database layer

Updated the Kotlin multiplatform plugin to version 2.1.0 and added the necessary dependency for the Kotlin-simple-logging library. Introduced new components, `Database.kt` and `Migrations.kt`, to manage database connections and migrations, along with adjustments to build configuration artifacts.
This commit is contained in:
2024-12-01 11:15:52 +01:00
parent 850f5dad8e
commit 3f7156e31f
6 changed files with 252 additions and 6 deletions

View File

@@ -1,6 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="template-js-1.0.0-SNAPSHOT">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="template-js-1.0.0-SNAPSHOT.jar" />
<root id="archive" name="template-js-1.0.0-SNAPSHOT.jar">
<element id="module-output" name="template.jsMain" />
</root>
</artifact>
</component>

View File

@@ -1,6 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="template-jvm-1.0.0-SNAPSHOT">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="template-jvm-1.0.0-SNAPSHOT.jar" />
<root id="archive" name="template-jvm-1.0.0-SNAPSHOT.jar">
<element id="module-output" name="template.jvmMain" />
</root>
</artifact>
</component>

View File

@@ -1,7 +1,7 @@
import org.jetbrains.kotlin.gradle.plugin.KotlinJsCompilerType
plugins {
kotlin("multiplatform") version "2.0.21"
kotlin("multiplatform") version "2.1.0"
}
group = "nl.astraeus"
@@ -19,9 +19,7 @@ repositories {
kotlin {
jvmToolchain(17)
jvm {
withJava()
}
jvm()
js {
binaries.executable()
browser {
@@ -33,6 +31,7 @@ kotlin {
sourceSets {
val commonMain by getting {
dependencies {
api("nl.astraeus:kotlin-simple-logging:1.1.1")
api("nl.astraeus:kotlin-css-generator:1.0.10")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.0")

View File

@@ -0,0 +1,37 @@
package tmpl
import com.zaxxer.hikari.HikariConfig
import nl.astraeus.logger.Logger
import tmpl.db.Database
val log = Logger()
fun main() {
Thread.currentThread().uncaughtExceptionHandler = Thread.UncaughtExceptionHandler { t, e ->
log.warn(e) {
e.message
}
}
Runtime.getRuntime().addShutdownHook(
object : Thread() {
override fun run() {
Database.vacuumDatabase()
Database.closeDatabase()
}
}
)
Class.forName("nl.astraeus.jdbc.Driver")
Database.initialize(HikariConfig().apply {
driverClassName = "nl.astraeus.jdbc.Driver"
jdbcUrl = "jdbc:stat:webServerPort=6001:jdbc:sqlite:data/${REPO_NAME}.db"
username = "sa"
password = ""
maximumPoolSize = 25
isAutoCommit = false
validate()
})
}

View File

@@ -0,0 +1,101 @@
package tmpl.db
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import java.sql.Connection
import java.util.*
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.collections.set
import kotlin.use
private val currentConnection = ThreadLocal<Connection>()
fun <T> transaction(
block: (Connection) -> T
): T {
val hasConnection = currentConnection.get() != null
var oldConnection: Connection? = null
if (!hasConnection) {
currentConnection.set(Database.getConnection())
/*
} else if (scope == TxScope.REQUIRES_NEW) {
oldConnection = currentConnection.get()
currentConnection.set(Database.getConnection())
*/
}
val connection = currentConnection.get()
try {
val result = block(connection)
connection.commit()
return result
} finally {
if (!hasConnection) {
currentConnection.set(oldConnection)
connection.close()
}
}
}
object Database {
var ds: HikariDataSource? = null
fun initialize(config: HikariConfig) {
val properties = Properties()
properties["journal_mode"] = "WAL"
config.dataSourceProperties = properties
config.addDataSourceProperty("cachePrepStmts", "true")
config.addDataSourceProperty("prepStmtCacheSize", "250")
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048")
ds = HikariDataSource(config)
Migrations.databaseVersionTableCreated = AtomicBoolean(false)
Migrations.updateDatabaseIfNeeded()
}
fun getConnection() = ds?.connection ?: error("Database has not been initialized!")
fun vacuumDatabase() {
getConnection().use {
it.autoCommit = true
it.prepareStatement("VACUUM").use { ps ->
ps.executeUpdate()
}
}
}
fun closeDatabase() {
ds?.close()
}
init {
val properties = Properties()
properties["journal_mode"] = "WAL"
val config = HikariConfig().apply {
driverClassName = "nl.astraeus.jdbc.Driver"
jdbcUrl = "jdbc:stat:webServerPort=6001:jdbc:sqlite:data/daw3.db"
username = "sa"
password = ""
maximumPoolSize = 25
isAutoCommit = false
dataSourceProperties = properties
validate()
}
config.addDataSourceProperty("cachePrepStmts", "true")
config.addDataSourceProperty("prepStmtCacheSize", "250")
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048")
ds = HikariDataSource(config)
}
}

View File

@@ -0,0 +1,105 @@
package tmpl.db
import tmpl.log
import java.sql.Connection
import java.sql.SQLException
import java.sql.Timestamp
import java.util.concurrent.atomic.AtomicBoolean
sealed class Migration {
class Query(
val query: String
) : Migration() {
override fun toString(): String {
return query
}
}
class Code(
val code: (Connection) -> Unit
) : Migration() {
override fun toString(): String {
return code.toString()
}
}
}
val DATABASE_MIGRATIONS = arrayOf<Migration>(
Migration.Query(
"""
CREATE TABLE DATABASE_VERSION (
ID INTEGER PRIMARY KEY,
QUERY TEXT,
EXECUTED TIMESTAMP
)
""".trimIndent()
),
Migration.Query("SELECT sqlite_version()"),
)
object Migrations {
var databaseVersionTableCreated = AtomicBoolean(false)
fun updateDatabaseIfNeeded() {
try {
transaction { con ->
con.prepareStatement(
"""
SELECT MAX(ID) FROM DATABASE_VERSION
""".trimIndent()
).use { ps ->
ps.executeQuery().use { rs ->
databaseVersionTableCreated.compareAndSet(false, true)
if(rs.next()) {
val maxId = rs.getInt(1)
for (index in maxId + 1..<DATABASE_MIGRATIONS.size) {
executeMigration(index)
}
}
}
}
}
} catch (e: SQLException) {
if (databaseVersionTableCreated.compareAndSet(false, true)) {
executeMigration(0)
updateDatabaseIfNeeded()
} else {
throw e
}
}
}
private fun executeMigration(index: Int) {
transaction { con ->
log.debug {
"Executing migration $index - [${DATABASE_MIGRATIONS[index]}]"
}
val description = when(
val migration = DATABASE_MIGRATIONS[index]
) {
is Migration.Query -> {
con.prepareStatement(migration.query).use { ps ->
ps.execute()
}
migration.query
}
is Migration.Code -> {
migration.code(con)
migration.code.toString()
}
}
con.prepareStatement("INSERT INTO DATABASE_VERSION VALUES (?, ?, ?)").use { ps ->
ps.setInt(1, index)
ps.setString(2, description)
ps.setTimestamp(3, Timestamp(System.currentTimeMillis()))
ps.execute()
}
}
}
}