Refactor project to focus on Markdown parser development. Removed unused database and template-related code, added Markdown parsing functionality, and updated build configuration.

This commit is contained in:
2025-12-02 19:44:45 +01:00
parent a8fcabc571
commit 63c24f6355
19 changed files with 661 additions and 253 deletions

1
.gitignore vendored
View File

@@ -44,3 +44,4 @@ bin/
### .kotlin ###
.kotlin
kotlin-js-store
gradle.properties

1
.idea/.name generated
View File

@@ -1 +0,0 @@
template

View File

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

View File

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

View File

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

View File

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

2
.idea/misc.xml generated
View File

@@ -3,7 +3,7 @@
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" project-jdk-name="17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
<component name="accountSettings">

View File

@@ -1,9 +1,14 @@
import com.vanniktech.maven.publish.SonatypeHost
plugins {
kotlin("multiplatform") version "2.1.10"
kotlin("multiplatform") version "2.2.21"
signing
id("org.jetbrains.dokka") version "2.0.0"
id("com.vanniktech.maven.publish") version "0.31.0"
}
group = "nl.astraeus"
version = "0.1.0-SNAPSHOT"
version = "1.0.0"
repositories {
mavenCentral()
@@ -16,45 +21,80 @@ repositories {
}
kotlin {
jvmToolchain(17)
jvmToolchain(11)
jvm()
js {
binaries.executable()
browser {
distribution {
outputDirectory.set(File("$projectDir/web/"))
}
}
binaries.library()
browser {}
}
sourceSets {
val commonMain by getting {
dependencies {
api("nl.astraeus:kotlin-simple-logging:1.1.1")
api("nl.astraeus:kotlin-css-generator:1.0.10")
}
}
val commonTest by getting
val jvmMain by getting {
val commonTest by getting {
dependencies {
implementation("io.undertow:undertow-core:2.3.14.Final")
implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:0.11.0")
implementation("org.xerial:sqlite-jdbc:3.32.3.2")
implementation("com.zaxxer:HikariCP:4.0.3")
implementation("nl.astraeus:simple-jdbc-stats:1.6.1") {
exclude(group = "org.slf4j", module = "slf4j-api")
}
}
}
val jvmTest by getting {
dependencies {
}
}
val jsMain by getting {
dependencies {
implementation("nl.astraeus:kotlin-komponent:1.2.5")
implementation(kotlin("test"))
}
}
val jvmTest by getting
val jsTest by getting
}
}
publishing {
repositories {
maven {
name = "gitea"
setUrl("https://gitea.astraeus.nl/api/packages/rnentjes/maven")
credentials {
val giteaUsername: String? by project
val giteaPassword: String? by project
username = giteaUsername
password = giteaPassword
}
}
mavenLocal()
}
}
tasks.withType<AbstractPublishToMaven> {
dependsOn(tasks.withType<Sign>())
}
signing {
sign(publishing.publications)
}
mavenPublishing {
publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
signAllPublications()
coordinates(group.toString(), name, version.toString())
pom {
name = "markdown-parser"
description = "Markdown parser"
inceptionYear = "2025"
url = "https://gitea.astraeus.nl/rnentjes/markdown-parser"
licenses {
license {
name = "MIT"
url = "https://gitea.astraeus.nl/rnentjes/markdown-parser"
}
}
developers {
developer {
id = "rnentjes"
name = "Rien Nentjes"
email = "info@nentjes.com"
}
}
scm {
url = "https://gitea.astraeus.nl/rnentjes/markdown-parser"
}
}
}

View File

@@ -1 +0,0 @@
kotlin.code.style=official

View File

@@ -1,5 +1,21 @@
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0"
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
val REPO_NAME = "dummy so the gitea template compiles, please remove"
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
}
rootProject.name = "markdown-parser"

View File

@@ -0,0 +1,77 @@
package nl.astraeus.wiki.parser
sealed class MarkdownPart {
data object NewLine : MarkdownPart()
data object PageBreak : MarkdownPart()
sealed class ParagraphPart() {
data class Text(
val text: String
) : ParagraphPart()
data object LineBreak : ParagraphPart()
data class Link(
val url: String,
val label: String? = null,
val title: String? = null,
) : ParagraphPart()
data class Image(
val alt: String,
val src: String,
val url: String? = null,
) : ParagraphPart()
data class Bold(
val text: String
) : ParagraphPart()
data class Italic(
val text: String
) : ParagraphPart()
class BoldItalic(
val text: String
) : ParagraphPart()
class StrikeThrough(
val text: String
) : ParagraphPart()
class InlineCode(
val text: String
) : ParagraphPart()
}
data class Paragraph(
val parts: List<ParagraphPart>
) : MarkdownPart()
data class Header(
val text: String,
val size: Int
) : MarkdownPart()
data class UnorderedList(
val lines: List<String>,
) : MarkdownPart()
data class OrderedList(
val lines: List<String>,
) : MarkdownPart()
data class CodeBlock(
val text: String,
val language: String
) : MarkdownPart()
data class Table(
val headers: List<String>,
val rows: List<List<String>>,
) : MarkdownPart()
class Ruler() : MarkdownPart()
}

View File

@@ -0,0 +1,174 @@
package nl.astraeus.wiki.parser
import nl.astraeus.wiki.parser.MarkdownPart.ParagraphPart.*
private enum class ParType {
TEXT,
LINK_LABEL,
LINK_URL,
LINK_TITLE,
LINK_END,
BOLD,
ITALIC,
BOLD_ITALIC,
STRIKETHROUGH,
INLINE_CODE,
IMAGE_ALT,
IMAGE_SRC,
LINK_IMAGE_ALT,
LINK_IMAGE_SRC,
LINK_IMAGE_LINK,
}
private typealias ParagraphData = MutableMap<ParType, String>
private data class ParState(
val fromType: ParType,
val text: String,
val toType: ParType,
val out: (ParagraphData) -> MarkdownPart.ParagraphPart? = { _ -> null }
)
private val states = listOf(
// Image with link
ParState(ParType.TEXT, "[![", ParType.LINK_IMAGE_ALT) { data ->
Text(data[ParType.TEXT]!!)
},
ParState(ParType.LINK_IMAGE_ALT, "](", ParType.LINK_IMAGE_SRC),
ParState(ParType.LINK_IMAGE_SRC, ")](", ParType.LINK_IMAGE_LINK),
ParState(ParType.LINK_IMAGE_LINK, ")", ParType.TEXT) { data ->
Image(
data[ParType.LINK_IMAGE_ALT]!!,
data[ParType.LINK_IMAGE_SRC]!!,
data[ParType.LINK_IMAGE_LINK],
)
},
// Image without link
ParState(ParType.TEXT, "![", ParType.IMAGE_ALT) { data ->
Text(data[ParType.TEXT]!!)
},
ParState(ParType.IMAGE_ALT, "](", ParType.IMAGE_SRC),
ParState(ParType.IMAGE_SRC, ")", ParType.TEXT) { data ->
Image(
data[ParType.IMAGE_ALT]!!,
data[ParType.IMAGE_SRC]!!,
)
},
// Links
ParState(ParType.TEXT, "[", ParType.LINK_LABEL) { data ->
Text(data[ParType.TEXT]!!)
},
ParState(ParType.LINK_LABEL, "](", ParType.LINK_URL),
ParState(ParType.LINK_LABEL, "]", ParType.LINK_URL) { data ->
Text(data[ParType.LINK_LABEL]!!)
},
ParState(ParType.LINK_URL, ")", ParType.TEXT) { data ->
Link(data[ParType.LINK_URL]!!, data[ParType.LINK_LABEL])
},
ParState(ParType.LINK_URL, "\"", ParType.LINK_TITLE),
ParState(ParType.LINK_TITLE, "\"", ParType.LINK_END),
ParState(ParType.LINK_END, ")", ParType.TEXT) { data ->
Link(
data[ParType.LINK_URL]!!,
data[ParType.LINK_LABEL],
data[ParType.LINK_TITLE],
)
},
ParState(ParType.TEXT, "***", ParType.BOLD_ITALIC) { data ->
Text(data[ParType.TEXT]!!)
},
ParState(ParType.BOLD_ITALIC, "***", ParType.TEXT) { data ->
BoldItalic(data[ParType.BOLD_ITALIC]!!)
},
ParState(ParType.TEXT, "~~", ParType.STRIKETHROUGH) { data ->
Text(data[ParType.TEXT]!!)
},
ParState(ParType.STRIKETHROUGH, "~~", ParType.TEXT) { data ->
StrikeThrough(data[ParType.STRIKETHROUGH]!!)
},
ParState(ParType.TEXT, "**", ParType.BOLD) { data ->
Text(data[ParType.TEXT]!!)
},
ParState(ParType.BOLD, "**", ParType.TEXT) { data ->
Bold(data[ParType.BOLD]!!)
},
ParState(ParType.TEXT, "*", ParType.ITALIC) { data ->
Text(data[ParType.TEXT]!!)
},
ParState(ParType.ITALIC, "*", ParType.TEXT) { data ->
BoldItalic(data[ParType.ITALIC]!!)
},
ParState(ParType.TEXT, "`", ParType.INLINE_CODE) { data ->
Text(data[ParType.TEXT]!!)
},
ParState(ParType.INLINE_CODE, "`", ParType.TEXT) { data ->
InlineCode(data[ParType.INLINE_CODE]!!)
},
)
private fun String.test(index: Int, value: String): Boolean {
return this.length > index + value.length && this.substring(index, index + value.length) == value
}
fun parseParagraph(text: String): MarkdownPart.Paragraph {
val result = mutableListOf<MarkdownPart.ParagraphPart>()
val buffer = StringBuilder()
var type = ParType.TEXT
val data: ParagraphData = mutableMapOf()
var index = 0
var activeStates = states.filter { it.fromType == type }
while (index < text.length) {
var found = false
for (state in activeStates) {
if (state.fromType == type && text.test(index, state.text)) {
data[state.fromType] = buffer.toString()
buffer.clear()
state.out(data)?.let {
if (it !is Text || it.text.isNotBlank()) {
result.add(it)
}
}
type = state.toType
index += state.text.length
found = true
activeStates = states.filter { it.fromType == type }
break
}
}
if (!found) {
val ch = text[index]
if (ch == '\n') {
// Markdown hard line break: two or more spaces at end of line
if (buffer.length >= 2 && buffer.endsWith(" ")) {
val textBefore = buffer.substring(0, buffer.length - 2)
if (textBefore.isNotEmpty()) {
result.add(Text(textBefore))
}
result.add(LineBreak)
buffer.clear()
} else {
// Keep original behavior for soft breaks (collapse later in HTML)
buffer.append(ch)
}
} else {
buffer.append(ch)
}
index++
}
}
if (buffer.isNotEmpty()) {
result.add(Text(buffer.toString()))
}
return MarkdownPart.Paragraph(result)
}

View File

@@ -0,0 +1,166 @@
package nl.astraeus.wiki.parser
enum class MarkdownType {
CODE,
PARAGRAPH,
ORDERED_LIST,
UNORDERED_LIST,
TABLE,
}
fun markdown(text: String): List<MarkdownPart> {
val lines = text.lines()
val parts = mutableListOf<MarkdownPart>()
var language = ""
var type = MarkdownType.PARAGRAPH
var listIndex = 1
var index = 0
val buffer = StringBuilder()
fun parseBuffer() {
if (buffer.isNotBlank()) {
parts.addAll(handleBuffer(type, buffer.toString(), language))
}
buffer.clear()
type = MarkdownType.PARAGRAPH
language = ""
}
while (index < lines.size) {
val rawLine = lines[index]
val line = rawLine.trim()
//println("BUFFER [${buffer.length}] TYPE ${type} \t LINE - ${line}")
when {
type == MarkdownType.ORDERED_LIST -> {
if (!line.startsWith("${listIndex++}.")) {
parseBuffer()
continue
} else {
buffer.append(line.substring(2))
buffer.append("\n")
}
}
type == MarkdownType.UNORDERED_LIST -> {
if (!line.startsWith("- ") &&
!line.startsWith("* ")
) {
parseBuffer()
continue
} else {
buffer.append(line.substring(2))
buffer.append("\n")
}
}
type == MarkdownType.TABLE -> {
if (!line.startsWith("|")) {
parseBuffer()
continue
} else {
buffer.append(line)
buffer.append("\n")
}
}
type == MarkdownType.PARAGRAPH && line.isBlank() -> {
buffer.append("\n")
parseBuffer()
}
line.startsWith("```") -> {
if (type != MarkdownType.CODE) {
parseBuffer()
type = MarkdownType.CODE
language = line.substring(3).trim()
} else {
parseBuffer()
}
}
type == MarkdownType.CODE -> {
buffer.append(rawLine)
buffer.append("\n")
index++
continue
}
line.startsWith("1.") -> {
parseBuffer()
type = MarkdownType.ORDERED_LIST
listIndex = 2
buffer.append(line.substring(2))
buffer.append("\n")
}
line.startsWith("- ") || line.startsWith("* ") -> {
parseBuffer()
type = MarkdownType.UNORDERED_LIST
buffer.append(line.substring(2))
buffer.append("\n")
}
line.startsWith("|") -> {
parseBuffer()
type = MarkdownType.TABLE
buffer.append(line)
buffer.append("\n")
}
line.startsWith("---") -> {
parseBuffer()
parts.add(MarkdownPart.Ruler())
}
line.startsWith("#") -> {
parseBuffer()
val headerLevel = line.takeWhile { it == '#' }.length
val headerText = line.substring(headerLevel).trim()
parts.add(MarkdownPart.Header(headerText, headerLevel))
}
line == "[break]" -> {
parseBuffer()
parts.add(MarkdownPart.PageBreak)
}
else -> {
// Preserve trailing spaces for hard line breaks (two spaces at end of line)
buffer.append(rawLine)
buffer.append("\n")
}
}
index++
}
parseBuffer()
return parts
}
private fun handleBuffer(
type: MarkdownType,
text: String,
language: String = ""
): List<MarkdownPart> = when (type) {
MarkdownType.CODE -> {
listOf(MarkdownPart.CodeBlock(text, language))
}
MarkdownType.PARAGRAPH -> {
listOf(parseParagraph(text))
}
MarkdownType.ORDERED_LIST -> {
listOf(MarkdownPart.OrderedList(text.lines()))
}
MarkdownType.UNORDERED_LIST -> {
listOf(MarkdownPart.UnorderedList(text.lines()))
}
MarkdownType.TABLE -> {
parseTable(text)
}
}

View File

@@ -0,0 +1,48 @@
package nl.astraeus.wiki.parser
fun parseTable(text: String): List<MarkdownPart> {
val lines = text.lines().map { it.trim() }.filter { it.isNotEmpty() }
fun parseCells(line: String): List<String> {
val trimmed = line.trim().trim('|')
return if (trimmed.isEmpty()) emptyList() else trimmed.split("|").map { it.trim() }
}
fun isSeparatorRow(cells: List<String>): Boolean {
if (cells.isEmpty()) return false
return cells.all { cell ->
val dashCount = cell.count { it == '-' }
val cleaned = cell.replace("-", "").replace("|", "").trim()
dashCount >= 3 && cleaned.isEmpty()
}
}
return if (lines.size < 2) {
// Not enough lines to be a table, fallback to code block
listOf(MarkdownPart.CodeBlock(text, "table"))
} else {
val headerCells = parseCells(lines.first())
val sepCells = parseCells(lines[1])
if (headerCells.isEmpty() || !isSeparatorRow(sepCells)) {
// Invalid table format, fallback to code block
listOf(MarkdownPart.CodeBlock(text, "table"))
} else {
val colCount = headerCells.size
val rows = mutableListOf<List<String>>()
for (i in 2 until lines.size) {
val rowCells = parseCells(lines[i]).toMutableList()
// Normalize column count to headers size
if (rowCells.size < colCount) {
while (rowCells.size < colCount) rowCells.add("")
} else if (rowCells.size > colCount) {
// Trim extras
while (rowCells.size > colCount) rowCells.removeAt(rowCells.lastIndex)
}
rows.add(rowCells)
}
listOf(MarkdownPart.Table(headers = headerCells, rows = rows))
}
}
}

View File

@@ -0,0 +1,50 @@
package nl.astraeus.markdown.parser
import nl.astraeus.wiki.parser.MarkdownPart
import nl.astraeus.wiki.parser.markdown
import kotlin.test.Test
class ParseTest {
@Test
fun testParagraph() {
val input = """
Dit is een **test**, laat ***mij*** maar eens zien!
link: [NOS](www.nos.nl "Nos title") of zo.
- link: [NU](www.nu.nl "Nu site") of zo.
""".trimIndent()
val md = markdown(input)
printMarkdownParts(md)
}
@Test
fun testImage() {
val input = """
[![test2](https://upload.wikimedia.org/wikipedia/commons.png)](https://upload.wikimedia.org/wikipedia/commons.png)
""".trimIndent()
val md = markdown(input)
printMarkdownParts(md)
}
private fun printMarkdownParts(md: List<MarkdownPart>) {
for (part in md) {
if (part is MarkdownPart.Paragraph) {
for (para in part.parts) {
println("PARA: ${para::class.simpleName} - ${para.toString().take(75)}")
}
} else {
println("PART: ${part::class.simpleName} - ${part.toString().take(75)}")
}
}
}
}

View File

@@ -0,0 +1,23 @@
package nl.astraeus.markdown.parser
import nl.astraeus.wiki.parser.MarkdownPart
import nl.astraeus.wiki.parser.parseParagraph
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class TestParagraph {
@Test
fun testBold() {
val input = "Text **bold** Text"
val result = parseParagraph(input)
assertEquals(3, result.parts.size)
assertTrue(result.parts[0] is MarkdownPart.ParagraphPart.Text)
assertTrue(result.parts[1] is MarkdownPart.ParagraphPart.Bold)
assertTrue(result.parts[2] is MarkdownPart.ParagraphPart.Text)
}
}

View File

@@ -1,39 +0,0 @@
package nl.astraeus.tmpl
import com.zaxxer.hikari.HikariConfig
import nl.astraeus.logger.Logger
import nl.astraeus.tmpl.db.Database
val log = Logger()
val REPO_NAME = "dummy so the gitea template compiles, please remove"
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/markdown-parser.db"
username = "sa"
password = ""
maximumPoolSize = 25
isAutoCommit = false
validate()
})
}

View File

@@ -1,73 +0,0 @@
package nl.astraeus.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())
}
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()
}
}

View File

@@ -1,105 +0,0 @@
package nl.astraeus.tmpl.db
import nl.astraeus.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()
}
}
}
}