https://kotlinlang.org logo
Join Slack
Powered by
# exposed
  • o

    Oleg Babichev

    04/09/2025, 5:57 PM
    🐙 Exposed 0.61.0 is now available 🐙
    kodee loving 4
    🙌 2
    exposed 2
    🙌🏾 1
    p
    c
    • 3
    • 2
  • d

    debop

    04/14/2025, 5:14 AM
    Exposed Ebook in Korean https://debop.notion.site/Kotlin-Exposed-Book-1ad2744526b080428173e9c907abdae2
    🙌 3
    ❤️ 2
  • d

    debop

    04/15/2025, 2:49 AM
    Alternives to JPA (Async/Non-Blocking)
    kodee loving 1
    🎉 1
    s
    • 2
    • 3
  • d

    debop

    04/15/2025, 3:30 PM
    I’m trying to implement an audit feature similar to Spring Data JPA (e.g., @CreatedDate, @LastModifiedDate). Here’s what I’ve tried so far: 1. Using EventHook – the updatedAt field is set after the SQL UPDATE, which can potentially cause infinite loops → not suitable. 2. Developed a custom interceptor inspired by EntityLifecycleInterceptor – but it’s unreliable since SPI order is not guaranteed. 3. Using property delegates – works, but it’s cumbersome since it must be applied to every auditable property manually. What I ideally want is something like this:
    Copy code
    interface AuditableEntity {
        val createdAt: Instant
        val updatedAt: Instant
    }
    And then assign values to createdAt and updatedAt automatically via an AuditableEntityLifecycleInterceptor, just before INSERT or UPDATE. Is this kind of approach feasible in Exposed 0.60.0?
    • 1
    • 2
  • d

    debop

    04/20/2025, 6:21 AM
    A6F3351B-C465-46C7-B1DB-7DF473A2CFD8.png
    🦜 1
    🥱 1
  • d

    debop

    04/26/2025, 11:06 PM
    I made
    Query.fetchBatchResults
    for Coroutines. returns
    Flow<List<ResultRow>>
    It support global unique id (eg. snowflakeId) which is not auto inc
    Copy code
    package io.bluetape4k.exposed.sql
    
    import kotlinx.coroutines.flow.Flow
    import kotlinx.coroutines.flow.channelFlow
    import org.jetbrains.exposed.dao.id.EntityID
    import org.jetbrains.exposed.sql.Column
    import org.jetbrains.exposed.sql.EntityIDColumnType
    import org.jetbrains.exposed.sql.FieldSet
    import org.jetbrains.exposed.sql.Op
    import org.jetbrains.exposed.sql.Query
    import org.jetbrains.exposed.sql.ResultRow
    import org.jetbrains.exposed.sql.SortOrder
    import org.jetbrains.exposed.sql.SqlExpressionBuilder.greater
    import org.jetbrains.exposed.sql.SqlExpressionBuilder.less
    import org.jetbrains.exposed.sql.and
    
    /**
     * [FieldSet] 에서 [SuspendedQuery.fetchBatchResultFlow] 메소드를 코루틴 환경에서 사용할 수 있도록 확장한 함수입니다.
     *
     * 이 함수를 사용하려면, 조회하는 첫번째 컬럼이 Int, Long 수형이어야 합니다.
     *
     *
    * // 10개씩 배치로 읽어온다 * val batchedIds: List<List<Int>> = ProductTable * .fetchBatchResultFlow(10) * .buffer(capacity = 2) * .map { rows -> rows.map { it[ProductTable.id].value } } * .toList() *
    Copy code
    */
    fun FieldSet.fetchBatchResultFlow(batch: Int = 1000, sortOrder: SortOrder = SortOrder.ASC): Flow<List<ResultRow>> =
        Query(this.source, null).fetchBatchResultFlow(batch, sortOrder)
    
    /**
     * [SuspendedQuery.fetchBatchResultFlow] 메소드를 코루틴 환경에서 사용할 수 있도록 확장한 함수입니다.
     *
     * 이 함수를 사용하려면, 조회하는 첫번째 컬럼이 Int, Long 수형이어야 합니다.
     *
     *
    * // 10개씩 배치로 읽어온다 * val batchedIds: List<List<Int>> = ProductTable * .select(ProductTable.id) * .fetchBatchResultFlow(10) * .buffer(capacity = 2) * .map { rows -> rows.map { it[ProductTable.id].value } } * .toList() *
    Copy code
    */
    fun Query.fetchBatchResultFlow(batch: Int = 1000, sortOrder: SortOrder = SortOrder.ASC): Flow<List<ResultRow>> =
        SuspendedQuery(this@fetchBatchResultFlow.set, null).fetchBatchResultFlow(batch, sortOrder)
    
    /**
     * [Query.fetchBatchedResults] 메소드를 코루틴 환경에서 사용할 수 있도록 확장한 함수를 제공하는 클래스입니다.
     */
    open class SuspendedQuery(set: FieldSet, where: Op<Boolean>? = null): Query(set, where) {
    
        /**
         * [Query.fetchBatchedResults] 메소드를 코루틴 환경에서 사용할 수 있도록 확장한 메소드입니다.
         *
         * 이 함수를 사용하려면, 조회하는 첫번째 컬럼이 Int, Long 수형이어야 합니다.
         *
         *
    * // 10개씩 배치로 읽어온다 * val batchedIds: List<List<Int>> = ProductTable * .select(ProductTable.id) * .fetchBatchResultFlow(10) * .buffer(capacity = 2) * .map { rows -> rows.map { it[ProductTable.id].value } } * .toList() *
    Copy code
    */
        fun fetchBatchResultFlow(batchSize: Int = 1000, sortOrder: SortOrder = SortOrder.ASC): Flow<List<ResultRow>> {
            require(batchSize > 0) { "Batch size should be greater than 0." }
            require(limit == null) { "A manual `LIMIT` clause should not be set. By default, `batchSize` will be used." }
            require(orderByExpressions.isEmpty()) {
                "A manual `ORDER BY` clause should not be set. By default, the auto-incrementing column will be used."
            }
    
            val comparatedColumn = try {
                set.source.columns.first()  //  { it.columnType.isAutoInc } // snowflakeId 같은 Global Unique ID 도 지원하기 위해
            } catch (_: NoSuchElementException) {
                throw UnsupportedOperationException("Batched select only works on tables with an auto-incrementing column")
            }
            limit = batchSize
            (orderByExpressions as MutableList).add(comparatedColumn to sortOrder)
            val whereOp = where ?: Op.TRUE
            val fetchInAscendingOrder =
                sortOrder in listOf(SortOrder.ASC, SortOrder.ASC_NULLS_FIRST, SortOrder.ASC_NULLS_LAST)
    
            fun toLong(autoIncVal: Any): Long = when (autoIncVal) {
                is EntityID<*> -> toLong(autoIncVal.value)
                is Int -> autoIncVal.toLong()
                else -> autoIncVal as Long
            }
    
            return channelFlow {
                var lastOffset = if (fetchInAscendingOrder) 0L else null
                while (true) {
                    val query = this@SuspendedQuery.copy().adjustWhere {
                        lastOffset?.let { lastOffset ->
                            whereOp and if (fetchInAscendingOrder) {
                                when (comparatedColumn.columnType) {
                                    is EntityIDColumnType<*> -> {
                                        (comparatedColumn as? Column<EntityID<Long>>)?.let {
                                            (it greater lastOffset)
                                        } ?: (comparatedColumn as? Column<EntityID<Int>>)?.let {
                                            (it greater lastOffset.toInt())
                                        } ?: (comparatedColumn greater lastOffset)
                                    }
                                    else -> (comparatedColumn greater lastOffset)
                                }
                            } else {
                                when (comparatedColumn.columnType) {
                                    is EntityIDColumnType<*> -> {
                                        (comparatedColumn as? Column<EntityID<Long>>)?.let {
                                            (it less lastOffset)
                                        } ?: (comparatedColumn as? Column<EntityID<Int>>)?.let {
                                            (it less lastOffset.toInt())
                                        } ?: (comparatedColumn less lastOffset)
                                    }
                                    else -> (comparatedColumn less lastOffset)
                                }
                            }
                        } ?: whereOp
                    }
                    val results = query.iterator().asSequence().toList()
                    if (results.isNotEmpty()) {
                        send(results)
                    }
                    if (results.size < batchSize) break
    
                    lastOffset = toLong(results.last()[comparatedColumn]!!)
                }
            }
        }
    }
  • m

    Matyáš Vítek

    05/10/2025, 6:18 PM
    Can I set a constraint for a unique pair of values in two different columns of one table?
    s
    • 2
    • 2
  • d

    debop

    05/12/2025, 2:30 PM
    Cache Strategies with Redisson & Exposed (Sync and Async) https://speakerdeck.com/debop/cache-strategies-with-redisson-and-exposed
  • s

    simon.vergauwen

    05/13/2025, 2:27 PM
    Check it out! Exposed 1.0.0-beta1 dropped with R2DBC support 🔥 kodee happy exposed
    🎉 18
    ⭐ 5
  • o

    Oleg Babichev

    05/14/2025, 7:49 AM
    🐙 Exposed 1.0 Beta is here with R2DBC support! 🐙 Check it out and share your feedback with us #100-beta-1
    👍 4
    🎉 1
  • g

    Grigory Panko

    05/14/2025, 3:08 PM
    Hi guys! Huge work on publishing R2DBC support! I have a problem: my table contains
    kotlinx.datetime.LocalDateTime
    column, and I'm trying to make queries to it. As long as the final
    ResultRow
    doesn't contain this column, everything works fine, but if Exposed needs to deserialize it, an exception occurs. Is it possible that there is some issue with
    TransactionCoroutineElement
    being detached when it shouldn't, or something like that? Stacktrace and some code snippets in thread.
    e
    d
    s
    • 4
    • 6
  • d

    debop

    05/19/2025, 11:49 AM
    https://kotlinlang.slack.com/archives/C0CG7E0A1/p1747647339915559?thread_ts=1747235321.240899&amp;cid=C0CG7E0A1
  • m

    martmists

    05/20/2025, 2:53 PM
    Getting an error when I define a custom primary key:
    Copy code
    e: org.jetbrains.kotlin.backend.common.BackendException: Backend Internal error: Exception during IR lowering
    File being compiled: /home/mart/git/sites/nintendo-posts/site-backend/src/main/kotlin/com/martmists/template/backend/database/table/PostAssetTable.kt
    The root cause java.lang.RuntimeException was thrown at: org.jetbrains.kotlin.backend.jvm.codegen.FunctionCodegen.generate(FunctionCodegen.kt:48)
        ...
    
    Caused by: java.lang.RuntimeException: Exception while generating code for:
    FUN CLASS_STATIC_INITIALIZER name:<clinit> visibility:public/*package*/ modality:FINAL <> () returnType:kotlin.Unit
        ...
    
    Caused by: java.lang.AssertionError: Unexpected IR element found during code generation. Either code generation for it is not implemented, or it should have been lowered:
    ERROR_CALL 'Unresolved reference: R|org/jetbrains/exposed/sql/Table.primaryKey|' type=IrErrorType([Error type: Unresolved type for <error>])
    	at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitElement(ExpressionCodegen.kt:946)
    	at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitElement(ExpressionCodegen.kt:135)
    	at org.jetbrains.kotlin.ir.visitors.IrElementVisitor$DefaultImpls.visitExpression(IrElementVisitor.kt:97)
    	at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitExpression(ExpressionCodegen.kt:135)
    	at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitExpression(ExpressionCodegen.kt:135)
    	at org.jetbrains.kotlin.ir.visitors.IrElementVisitor$DefaultImpls.visitErrorExpression(IrElementVisitor.kt:214)
    	at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitErrorExpression(ExpressionCodegen.kt:135)
    	at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitErrorExpression(ExpressionCodegen.kt:135)
    with code:
    Copy code
    package com.martmists.template.backend.database.table
    
    import org.jetbrains.exposed.sql.Table
    
    object PostAssetTable : Table() {
        val post = reference("post", PostTable)
        val asset = reference("asset", AssetTable)
    
        init {
            primaryKey = PrimaryKey(post, asset)
        }
    }
    • 1
    • 1
  • r

    rnett

    05/22/2025, 11:25 PM
    There was a change in the 1.0.0 betas to move a bunch of the core types from
    org.jetbrains.exposed.sql
    to
    org.jetbrains.exposed.v1.core
    . Is that going to be in the released version too? It wasn't listed in the changelog so it was quite surprising to me. It's also quite painful to migrate and frankly I don't see the upside.
    👍 2
    b
    • 2
    • 2
  • b

    Bogdan

    05/23/2025, 4:48 AM
    It seems to me that rdbc is not very well done, and there is a lot of duplication of code that will need to be maintained. And rdbc itself has become less necessary and a rare case since the JVM released Loom
  • d

    debop

    05/25/2025, 11:27 AM
    Examples for Exposed R2dbc (based 1.0.0-Beta-2) https://github.com/bluetape4k/exposed-r2dbc-workshop
  • s

    solonovamax

    05/27/2025, 4:06 PM
    hey, I'm looking for exposed extensions that add support for the following data types from postgres: • ranges (
    {int4,int8,num,ts,tz,date}{,multi}range
    ) • postgis data types are there any existing extensions for these, or will I have to build my own? I've already built my own for them, but I'm just wondering if there's smth that I've missed which already exists for either of those. if no adequate library exists for either, then I'm most likely going to open source my extension for those so others can benefit from them.
  • s

    solonovamax

    05/27/2025, 4:17 PM
    also, with the introduction of support for R2DBC, what's the performance of it compared to JDBC? is it worth considering switching to R2DBC from JDBC once the full 1.0.0 release has been made?
  • d

    debop

    05/28/2025, 1:38 PM
    When using Spring AOT,
    exposed-dao
    occur exception (use 1.0.0-beta-2)
    Copy code
    Exception in thread "main" java.lang.IllegalArgumentException: Resource must be a ClassPathResource that exists: class path resource [META-INF/services/org.jetbrains.exposed.v1.dao.id.EntityIDFactory]
    	at org.springframework.aot.hint.ResourceHints.registerResource(ResourceHints.java:129)
    	at org.jetbrains.exposed.v1.spring.boot.ExposedAotContribution.registerResourceHints(ExposedAotContribution.kt:62)
    	at org.jetbrains.exposed.v1.spring.boot.ExposedAotContribution.processAheadOfTime$lambda$2(ExposedAotContribution.kt:39)
    	at org.springframework.context.aot.BeanFactoryInitializationAotContributions.applyTo(BeanFactoryInitializationAotContributions.java:96)
    	at org.springframework.context.aot.ApplicationContextAotGenerator.lambda$processAheadOfTime$0(ApplicationContextAotGenerator.java:59)
    	at org.springframework.context.aot.ApplicationContextAotGenerator.withCglibClassHandler(ApplicationContextAotGenerator.java:68)
    	at org.springframework.context.aot.ApplicationContextAotGenerator.processAheadOfTime(ApplicationContextAotGenerator.java:54)
    	at org.springframework.context.aot.ContextAotProcessor.performAotProcessing(ContextAotProcessor.java:107)
    	at org.springframework.context.aot.ContextAotProcessor.doProcess(ContextAotProcessor.java:84)
    	at org.springframework.context.aot.ContextAotProcessor.doProcess(ContextAotProcessor.java:49)
    	at org.springframework.context.aot.AbstractAotProcessor.process(AbstractAotProcessor.java:83)
    	at org.springframework.boot.SpringApplicationAotProcessor.main(SpringApplicationAotProcessor.java:80)
  • m

    Maarten

    05/30/2025, 8:43 AM
    Hey guys, I posted an issue on YouTrack a couple days ago about an N+1 problem that's supposed to be fixed, according to the docs. We're testing a switch from Hibernate to Exposed on our backend and making sure this works is a big blocker. Could you tell me if I'm missing anything?
  • m

    Mark lindsay

    05/31/2025, 1:25 AM
    Hello and good evening
  • a

    Adam Jarvis

    06/05/2025, 4:24 PM
    Was curious, do others have thoughts on adjusting
    where
    to be closer to
    andWhere
    ?
    Copy code
    fun where(predicate: Op<Boolean>): Query {
        where?.let {
            error("WHERE clause is specified twice. Old value = '$it', new value = '$predicate'")
        }
        where = predicate
        return this
    }
    ^ to me, I end up just using
    andWhere
    everywhere because of the built in handling of chaining
    Copy code
    fun Query.andWhere(andPart: SqlExpressionBuilder.() -> Op<Boolean>) = adjustWhere {
        val expr = Op.build { andPart() }
        if (this == null) expr else this and expr
    }
    Why always throwing the error for chaining
    where
    ?
    l
    b
    • 3
    • 5
  • m

    Maarten

    06/13/2025, 1:55 PM
    Has anyone successfully managed to share transactions between spring jpa and exposed? Running exposed within a JPA @Transactional function, I get the error that there's no transaction in context
    b
    • 2
    • 3
  • k

    koji.lin

    06/18/2025, 11:45 PM
    Hi, I'm curious about how to handle coroutine and transaction well. There are some examples like

    https://www.youtube.com/watch?v=Y-iMbMro9tE▾

    have some information, but for the case like we want to wrap few repository call in the same transaction, I still have question. I'm using ktor so wanna make service suspend, also still use jdbc with suspend not r2dbc in beta.
    Copy code
    class HogeService {
      suspend fun getArticle(id: String){
        newSuspendedTransaction {
           val article = articleRepo.find(id)
           // what ever other repo
           viewCountRepo.increase(article.id)
        }
        // other suspend logic here, like fetching some info from remote
        ....
        return article
      }
    }
    
    suspend fun <T : Any> dbQuery(context: CoroutineContext?, statement: suspend () -> T?): T? =
        TransactionManager.currentOrNull()?.withSuspendTrnsaction(context, statement)
        ?:newSuspendedTransaction(context, statement = statement)
    
    class ArticleRepo {
      suspend fun find(id:String) {
         return dbQuery(Dispatchers.IO) {
               //....
         }
      }
    }
    // Same patter as ArticleRepo
    class ViewCountRepo...
    So this should be open transaction at service layer and pass transaction to repo to run on other Dispatcher. Is this usage correct? Especially I saw https://www.jetbrains.com/help/exposed/transactions.html#working-with-coroutines has phrase like below that maybe my example is wrong due to transaction may start from ktor's eventloop dispatcher then used in IO and IO may use different thread in Dispatchers.IO. Maybe this means two unrelated coroutines but ok for parent/child relationship? > Please note that such code remains blocking (as it still uses JDBC) and you should not try to share a transaction between multiple threads as it may lead to undefined behavior. So curious if it's correct or I should do in another way.
    • 1
    • 1
  • a

    Anatoly

    06/19/2025, 7:12 PM
    Hello. Help please. How to create custom Operator with question mark? Exposed thinks this question is parameter...
    No value specified for parameter 3
    Copy code
    class JsonContainsAnyExpression(
      expression: Expression<*>,
      elements: List<String>
    ) : CustomOperator<Boolean>("?|", BooleanColumnType(), expression, arrayLiteral(elements))
    
    fun Expression<*>.containsAny(elements: List<String>): JsonContainsAnyExpression =
      JsonContainsAnyExpression(this, elements)
    addLogger(StdSQLLogger) - если добавляешь логгер, вообще выдает:
    NullPointerException
  • p

    Paul Rule

    06/24/2025, 2:59 AM
    Hi all, I’m looking for some insight into this issue I’m having with my Ktor/exposed application… If I do the following in a KTOR route, after the
    newSuspendedTransaction
    block is close,
    TransactionManager.currentOrNull()
    returns a not null value - I would expect it to be null. If I remove the
    delay()
    call, it will work as expected.
    Copy code
    get("/") {
        require(TransactionManager.currentOrNull() == null) { "1 - transaction should not exist at the start" }
        newSuspendedTransaction {
          require(TransactionManager.currentOrNull() != null) { "2 - transaction should exist now" }
          delay(2000) // Simulate some processing time - if I remove this delay things will work as I expect
        }
        // this next line fails
        require(TransactionManager.currentOrNull() == null) { "3 - transaction should not exist at the end" }
        call.respondText("OK")
      }
    To make it even more mysterious I cannot get this scenario to fail in a test - the following code works fine, and
    TransactionManager.currentOrNull()
    returns null after the
    newSuspendedTransaction
    scope closes.
    Copy code
    suspend fun main() {
      val database = TestPostgresDatabase
    
      val fixtures = Fixtures(database).initialise()
    
      coroutineScope {
        launch(Dispatchers.IO) {
          require(TransactionManager.currentOrNull() == null) { "1 - transaction should not exist at the start" }
          newSuspendedTransaction {
            require(TransactionManager.currentOrNull() != null) { "2 - transaction should exist now" }
            delay(2000) // Simulate some processing time
          }
          require(TransactionManager.currentOrNull() == null) { "3 - transaction should not exist at the end" }
          println("Transaction completed successfully ${TransactionManager.currentOrNull()}")
        }.join()
      }
    }
    Can anyone tell me what I’m doing wrong or missing please? What I'm trying to is test if a transaction is already in progress or not using
    TransactionManager.currentOrNull()
    so I know when to initialise my connection with row level security parameters. For the most part its working well but there is this one condition where there is a network call - which must result in yielding similar to the delay above, and then I get left with a closed connection...
    a
    k
    • 3
    • 11
  • h

    Hildebrandt Tobias

    06/29/2025, 6:06 PM
    Hey I just wrote this and wonder if there is an easier way to achieve the same thing. Background is that the
    value: String
    comes from an API call over HTTP. So I want to always compare against a given
    String
    in the where clause. I can't filter after the fact in Kotlin, because it's part of a paging scheme. I tried poking AI about a better way, but they were no help in this case.
    Copy code
    fun <T : Any, V: T?> Column<V>.compareWithString(value: String): Op<Boolean> {
        if(columnType is EntityIDColumnType<*>){
            val checkType = (columnType as EntityIDColumnType<T>).idColumn.columnType
            val typedValue = when (checkType) {
                is IntegerColumnType -> value.toInt() as T
                is LongColumnType -> value.toLong() as T
                is UUIDColumnType -> UUID.fromString(value) as T
                is StringColumnType -> value as T
                is BooleanColumnType -> value.toBooleanStrictOrNull() as T
                else -> error("Unsupported column type for comparison: $columnType")
            }
            return (this as Column<EntityID<T>>) eq typedValue
        } else {
            val typedValue = when (columnType) {
                is IntegerColumnType -> value.toInt() as V
                is LongColumnType -> value.toLong() as V
                is UUIDColumnType -> UUID.fromString(value) as V
                is StringColumnType -> value as V
                is BooleanColumnType -> value.toBooleanStrictOrNull() as V
                else -> error("Unsupported column type for comparison: $columnType")
            }
            return this eq typedValue
        }
    }
    • 1
    • 1
  • m

    Mario Andhika

    06/30/2025, 3:57 AM
    Is r2dbc “worth it” for coroutine-based systems?
    👀 1
    a
    • 2
    • 1
  • o

    Oleg Babichev

    07/02/2025, 7:12 PM
    🐙 Exposed 1.0.0-beta-3 is now available 🐙
    🎉 7
  • m

    Mario Andhika

    07/03/2025, 12:36 PM
    For branding, should I use the squid logo (the one on GitHub readme) or the database with arrow logo?