postgresqlkotlinktorlocaldatekotlin-exposed

Error with Ktor and Exposed with LocalDate field in Postgres (ClassNotFoundException: kotlinx.datetime.Instant)


I have a small test table in Postgres with a DATE field, and data class in Ktor app. When mapping the database table row result to TestItem the 'updated' LocalDate field generated the error.

import DatabaseFactory.dbQuery
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpMethod
import io.ktor.http.HttpStatusCode
import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.plugins.cors.routing.CORS
import io.ktor.server.plugins.defaultheaders.DefaultHeaders
import io.ktor.server.response.respond
import io.ktor.server.routing.get
import io.ktor.server.routing.route
import io.ktor.server.routing.routing
import kotlinx.datetime.LocalDate
import kotlinx.serialization.Serializable
import org.jetbrains.exposed.v1.core.Column
import org.jetbrains.exposed.v1.core.ResultRow
import org.jetbrains.exposed.v1.core.StdOutSqlLogger
import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.datetime.date
import org.jetbrains.exposed.v1.jdbc.Database
import org.jetbrains.exposed.v1.jdbc.selectAll
import org.jetbrains.exposed.v1.jdbc.transactions.transaction

object DatabaseFactory{
    fun init() {
        val driver = "org.postgresql.Driver"
        val url = "jdbc:postgresql://192.168.0.XXX:5432/mydb"
        val user = "postgres"
        val password = "postgres"

        val db = Database.connect(url, driver, user = user, password = password)
        transaction {
            addLogger(StdOutSqlLogger)
        }
    }
    fun <T> dbQuery(block: () -> T): T =
        transaction { block() }
}

object TestTable: Table (name="test") {
    val id: Column<Int> = integer("id")
    val updated: Column<LocalDate> = date("updated")
}

@Serializable
data class TestItem(
    val id: Int,
    val updated: LocalDate
)

interface DAOFacade {
    suspend fun getId(id: Int): TestItem?
}

class DAOFacadeImpl : DAOFacade {
    private fun resultRowToTestId(row: ResultRow): TestItem = TestItem(
        id = row[TestTable.id],
        updated = row[TestTable.updated]
    )

    override suspend fun getId(id: Int): TestItem? {
        val foundId = dbQuery {
            TestTable
                .selectAll()
                .where { TestTable.id eq id }
                .map(::resultRowToTestId)
                .firstOrNull()
        }
        return foundId
    }
}

val dao: DAOFacade = DAOFacadeImpl()

fun main() {
    embeddedServer(Netty, host = "0.0.0.0", port = 8080, module = Application::module)
        .start(wait = true)
}

fun Application.module() {
    configureSerialization()
    DatabaseFactory.init()
    configureRouting()
    configureHTTP()
}

fun Application.configureSerialization() {
    install(ContentNegotiation) {
        json()
    }
}

fun Application.configureRouting() {
    routing {
        route("/") {
            get("/{id}") {
                val requestId = call.parameters["id"] ?: ""
                val idFound: TestItem? = dao.getId(requestId.toInt())

                if (idFound == null) {
                    call.respond(HttpStatusCode.BadRequest, message = "Not Found")
                } else {
                    call.respond(HttpStatusCode.OK, message = idFound)
                }
            }
        }
    }
}

fun Application.configureHTTP() {
    install(DefaultHeaders) {
        header("X-Engine", "Ktor") // will send this header with each response
    }
    install(CORS) {
        allowMethod(HttpMethod.Options)
        allowHeader(HttpHeaders.Authorization)
        allowHeader("MyCustomHeader")
        anyHost() // @TODO: Don't do this in production if possible. Try to limit it.
    }
}

My build.gradle.kts:

plugins {
    kotlin("jvm") version "2.2.0"
    kotlin("plugin.serialization") version "2.2.20-Beta1"
    id("io.ktor.plugin") version "3.2.2"
    application
}

application {
    mainClass.set("MainKt")
}

group = "net.karpenkov"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
}

dependencies {
    testImplementation(kotlin("test"))
    dependencies {
        implementation("org.slf4j:slf4j-api:2.0.3")
        implementation("org.jetbrains.exposed:exposed-core:1.0.0-beta-4")
        implementation("org.jetbrains.exposed:exposed-dao:1.0.0-beta-4")
        implementation("org.jetbrains.exposed:exposed-jdbc:1.0.0-beta-4")
        implementation("org.jetbrains.exposed:exposed-kotlin-datetime:1.0.0-beta-4")
        implementation("io.ktor:ktor-server-core-jvm:3.2.3")
        implementation("io.ktor:ktor-serialization-kotlinx-json-jvm:3.2.3")
        implementation("io.ktor:ktor-server-content-negotiation-jvm:3.2.3")
        implementation("org.postgresql:postgresql:42.5.1")
        implementation("io.ktor:ktor-server-cors-jvm:3.2.3")
        implementation("io.ktor:ktor-server-default-headers:3.2.3")
        implementation("io.ktor:ktor-server-host-common-jvm:3.2.3")
        implementation("io.ktor:ktor-server-auth-jvm:3.2.3")
        implementation("io.ktor:ktor-server-netty-jvm:3.2.3")
        implementation("io.ktor:ktor-server-auth:3.2.3")
        implementation("io.ktor:ktor-server-sessions:3.2.3")
        implementation("io.ktor:ktor-server-freemarker:3.2.3")
        implementation("ch.qos.logback:logback-classic:1.4.11")
        implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.7.1")
        implementation("io.ktor:ktor-server-status-pages:2.1.2")
        implementation("com.typesafe:config:1.4.2")
    }
}

tasks.test {
    useJUnitPlatform()
}

kotlin {
    jvmToolchain(24)
}

ktor {
    development = true
    fatJar {
        archiveFileName.set("testLocaleDate.jar")
    }
}

Run .jdks/azul-24.0.1/bin/java from Idea IDE

Error (from ./gradlew run):

Caused by: java.lang.ClassNotFoundException: kotlinx.datetime.Instant
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:580)
Caused by: java.lang.ClassNotFoundException: kotlinx.datetime.Instant

java.lang.NoClassDefFoundError: kotlinx/datetime/Instant
    at org.jetbrains.exposed.v1.datetime.KotlinLocalDateColumnType.longToLocalDate(KotlinDateColumnType.kt:229)
    at org.jetbrains.exposed.v1.datetime.KotlinLocalDateColumnType.valueFromDB(KotlinDateColumnType.kt:202)
    at org.jetbrains.exposed.v1.datetime.KotlinLocalDateColumnType.valueFromDB(KotlinDateColumnType.kt:187)
    at org.jetbrains.exposed.v1.core.ResultRow.rawToColumnValue(ResultRow.kt:99)
    at org.jetbrains.exposed.v1.core.ResultRow.getInternal$lambda$5$lambda$4$lambda$3(ResultRow.kt:87)
    at org.jetbrains.exposed.v1.core.vendors.DatabaseDialectKt.withDialect(DatabaseDialect.kt:152)
    at org.jetbrains.exposed.v1.core.ResultRow.getInternal$lambda$5(ResultRow.kt:86)
    at org.jetbrains.exposed.v1.core.ResultRow$ResultRowCache.cached(ResultRow.kt:218)

I can't find any documentation to troubleshoot this simple code. What might be a problem?


Solution

  • The latest 0.7.1 kotlinx-datetime deprecated Instant so the code fails in the runtime. The easiest solution is to switch to 0.7.1-0.6.x-compat version of the same library.

    From the README:

    Deprecation of Instant

    kotlinx-datetime versions earlier than 0.7.0 used to provide kotlinx.datetime.Instant and kotlinx.datetime.Clock. The Kotlin standard library started including its own, identical kotlin.time.Instant and kotlin.time.Clock, as it became evident that Instant was also useful outside the datetime contexts.

    Here is the recommended procedure for migrating from kotlinx-datetime version 0.6.x or earlier to 0.7.x:

    • First, simply try upgrading to 0.7.1. If your project has a dependency on kotlinx-datetime, but doesn't have dependencies on other libraries that are themselves reliant on an older kotlinx-datetime, you are good to go: the code should compile and run. This applies both to applications and to libraries!

    • If your project depends on other libraries that themselves use an older version of kotlinx-datetime, then your code may fail at runtime with a ClassNotFoundException for kotlinx.datetime.Instant or kotlinx.datetime.Clock, or maybe even fail to compile. In that case, please check if the affected libraries you have as dependencies have already published a new release adapted to use Instant and Clock from kotlin.time.

    • If you use kotlinx-serialization to serialize the Instant type, update that dependency to use 1.9.0 or a newer version.

    • If all else fails, use the compatibility release of kotlinx-datetime. Instead of the version 0.7.1, use 0.7.1-0.6.x-compat. This artifact still contains kotlinx.datetime.Instant and kotlinx.datetime.Clock, ensuring that third-party libraries reliant on them can still be used. This artifact is less straightforward to use than 0.7.1, so only resort to it when libraries you don't control require that the removed classes still exist.

    [...]

    As I mentioned above, the 4th point was the solution in my case.