diff --git a/.idea/artifacts/template_js_1_0_0_SNAPSHOT.xml b/.idea/artifacts/template_js_1_0_0_SNAPSHOT.xml index 5f495ea..fffbaa8 100644 --- a/.idea/artifacts/template_js_1_0_0_SNAPSHOT.xml +++ b/.idea/artifacts/template_js_1_0_0_SNAPSHOT.xml @@ -1,6 +1,8 @@ $PROJECT_DIR$/build/libs - + + + \ No newline at end of file diff --git a/.idea/artifacts/template_jvm_1_0_0_SNAPSHOT.xml b/.idea/artifacts/template_jvm_1_0_0_SNAPSHOT.xml index 5782466..d071410 100644 --- a/.idea/artifacts/template_jvm_1_0_0_SNAPSHOT.xml +++ b/.idea/artifacts/template_jvm_1_0_0_SNAPSHOT.xml @@ -1,6 +1,8 @@ $PROJECT_DIR$/build/libs - + + + \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 1267b7e..af3b15e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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") diff --git a/src/jvmMain/kotlin/tmpl/Main.kt b/src/jvmMain/kotlin/tmpl/Main.kt new file mode 100644 index 0000000..3133194 --- /dev/null +++ b/src/jvmMain/kotlin/tmpl/Main.kt @@ -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() + }) + +} diff --git a/src/jvmMain/kotlin/tmpl/db/Database.kt b/src/jvmMain/kotlin/tmpl/db/Database.kt new file mode 100644 index 0000000..b00ec1f --- /dev/null +++ b/src/jvmMain/kotlin/tmpl/db/Database.kt @@ -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() + +fun 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) + } + +} diff --git a/src/jvmMain/kotlin/tmpl/db/Migrations.kt b/src/jvmMain/kotlin/tmpl/db/Migrations.kt new file mode 100644 index 0000000..ce68eb2 --- /dev/null +++ b/src/jvmMain/kotlin/tmpl/db/Migrations.kt @@ -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.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.. + 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() + } + } + } + +}