Exposed: [Postgres] enum ClassCastException

Created on 24 Jul 2019  路  12Comments  路  Source: JetBrains/Exposed

Lib versions checked: 0.15.1, 0.16.2

Hi :) I've been looking through the existing (mostly closed) issues, but they seem old and only partially related.

Using the DAO API, and following examples in the docs, I've had enums working fine, until trying to set a column (creating and reading records has been ok).

Now, after setting a column value, the transaction flush throws an exception:

[...] PgEnum cannot be cast to java.lang.Enum

Hopefully I'm just doing something wrong, but it seems buried pretty deep in the DAO code.
I can provide more of my code, and more stack trace if it helps :)

My code in question looks like:

object ChallengeTable : IntIdTable() {
  val createdAt = datetime("createdAt")
  val slug = varchar("slug", 256).uniqueIndex()
  val status = ChallengeStatus.pgColumn(this, "status")
  val entryId = varchar("entryId", 64)
}

class ExposedChallenge(id: EntityID<Int>) : IntEntity(id) {
  companion object : IntEntityClass<ExposedChallenge>(ChallengeTable)

  var createdAt by ChallengeTable.createdAt
  var slug by ChallengeTable.slug
  var status by ChallengeTable.status
  var entryId by ChallengeTable.entryId
}

enum class ChallengeStatus {
  draft,
  live,
  completed,
  archive;

  companion object {
    const val dbName = "challenge_status"

    fun pgColumn(table: Table, name: String) = table.customEnumeration(
      name = name,
      sql = dbName,
      fromDb = { it.fromPg() },
      toDb = { it.toPg() }
    )

    private fun Any.fromPg() = valueOf(this as String)
    private fun ChallengeStatus?.toPg() = PgEnum(this)
  }

  class PgEnum(enumValue: ChallengeStatus?) : PGobject() {
    init {
      value = enumValue?.name
      type = dbName
    }
  }
}

And the call site (simplified):

transaction {
  ExposedChallenge
    .find { (ChallengeTable.slug eq "exampleSlug") }
    .first().apply {
      status = challengeStatus
    }
}

The first few stack slices:

java.lang.ClassCastException: ChallengeStatus$PgEnum cannot be cast to java.lang.Enum
        at org.jetbrains.exposed.sql.Table$customEnumeration$1.notNullValueToDB(Table.kt:235)
        at org.jetbrains.exposed.sql.IColumnType$DefaultImpls.nonNullValueToString(ColumnType.kt:51)
        at org.jetbrains.exposed.sql.ColumnType.nonNullValueToString(ColumnType.kt:60)
        at org.jetbrains.exposed.sql.IColumnType$DefaultImpls.valueToString(ColumnType.kt:43)
        at org.jetbrains.exposed.sql.ColumnType.valueToString(ColumnType.kt:60)
        at org.jetbrains.exposed.sql.QueryBuilder$registerArguments$1.invoke(Expression.kt:19)

Most helpful comment

@abubics , thank you for your sample. It helped a lot and I was able to find the bug.

It will be fixed in upcoming 0.18.1.

All 12 comments

Workaround update: avoiding the DAO DSL with all the same existing classes works :)

i.e.:

    transaction {
      ChallengeTable.update({ ChallengeTable.slug eq challengeSlug }) {
        it[status] = challengeStatus
      }
    }

What is challengeStatus in a fail example? PgEnum or ChallengeStatus?

Ah, sorry, it's a ChallengeStatus (specifically live in this case).

@abubics, I was trying to reproduce an issue by changing existing customEnumeration test but without any success.

Could you please share a complete sample or patch for the test case?

I'll try, but probably don't have time until after the weekend... I'm moving house on Wednesday 馃槄 thanks!

Facing the same issue. Try the DAO update not the DSL update. Not very hard to reproduce.

@bvjebin, there is test with both DSL and DAO cases. That's why I ask for a reproducable sample

Working on it now :)

I might be wrong, I don't see a transaction in the test. Does that make any difference?

What I have observed is notNullValueToDB of customEnumeration in Table.kt is called twice. The first time it is called, the value argument is an Enum and the second time it is called, it is of type PGEnum which is returned from the customEnumeration I have defined as my column.

It is happening only for update. Not for insert.

Exposed Version: 0.11.2

Transaction Helper

class TransactionHelper {
    companion object {
        private val logger: Logger = LoggerFactory.getLogger("TransactionHelper")
        fun <T> execute(fn: () -> T): T {
            return transaction {
                try {
                    addLogger(StdOutSqlLogger)
                    fn()
                } catch (exception: Exception) {
                    val original = (exception as? ExposedSQLException)?.cause
                    //TODO: Need to build comprehensive sql error handling based on cause.SQLState value
                    when (original) {
                        is SQLIntegrityConstraintViolationException ->
                            TransactionHelper.logger.error("which one SQL constraint violated")
                        is BatchUpdateException ->
                            TransactionHelper.logger.error("SQL constraint violated")
                        else ->
                            TransactionHelper.logger.error("Unknown sql exception ${exception.message}")
                    }
                    TransactionManager.current().rollback()
                    throw exception
                } catch (error: Error) {
                    TransactionHelper.logger.error("Error occurred: ${error.message}")
                    throw error
                }
            }
        }
    }
}

In the controller:

val response = TransactionHelper.execute {
    val userProfile = UserProfile.findById(id.toInt(10))
    if (userProfile != null) {
          UserProfileSerializer.serialize(UserProfile.update(userProfile, serializedUserProfile))
    } else
          throw WebApplicationException(404)
}

In the model:

enum class ActivityLevel { ACTIVE, SEMI_ACTIVE, SEDENTARY }
object UserProfiles : BaseIntIdTable("user_profiles") {
    val activityLevel = customEnumeration("activity_level",
            "activity_level",
            { ActivityLevel.valueOf(it.toString()) },
            { it -> PGEnum("activity_level", it) }).nullable()
    val isDeleted = bool("is_deleted").default(false)
}

class UserProfile(id: EntityID<Int>) : BaseIntEntity(id, UserProfiles) {
    companion object : BaseIntEntityClass<UserProfile>(UserProfiles) {
        fun update(row: UserProfile, request: UserProfileSerializer): UserProfile {
        with(row) {
            activityLevel = request.activityLevel
            isDeleted = request.isDeleted
        }
        return row
            }
    }

    var activityLevel by UserProfiles.activityLevel
    var isDeleted by UserProfiles.isDeleted
}

This is what I am trying.. See if this helps.

Sorry for the delay, here's a minimal sample :D
https://github.com/abubics/exposed-enum

@abubics , thank you for your sample. It helped a lot and I was able to find the bug.

It will be fixed in upcoming 0.18.1.

Awesome, glad to have been helpful ^_^

Was this page helpful?
0 / 5 - 0 ratings

Related issues

blackmo18 picture blackmo18  路  3Comments

vasily-kirichenko picture vasily-kirichenko  路  4Comments

ncobc picture ncobc  路  3Comments

mgmeiner picture mgmeiner  路  3Comments

kszymanski85 picture kszymanski85  路  4Comments